From 722de4836b475d082816164a55d6998bdb2370df Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Thu, 21 Nov 2019 09:58:48 +0100 Subject: [PATCH 001/240] Improve the testing around the new --fail-under flag --- pylint/lint.py | 1 + .../fail_under_minus6.py | 0 .../fail_under_plus6.py | 0 tests/test_self.py | 48 +++++++++++++++++++ tests/unittest_fail_threshold.py | 20 -------- 5 files changed, 49 insertions(+), 20 deletions(-) rename tests/{input => regrtest_data}/fail_under_minus6.py (100%) rename tests/{input => regrtest_data}/fail_under_plus6.py (100%) delete mode 100644 tests/unittest_fail_threshold.py diff --git a/pylint/lint.py b/pylint/lint.py index c85d512ecc6..6cc0c0a8924 100644 --- a/pylint/lint.py +++ b/pylint/lint.py @@ -1774,6 +1774,7 @@ def __init__(self, args, reporter=None, do_exit=True): if linter.config.exit_zero: sys.exit(0) else: + print(score_value, linter.config.fail_under) if score_value and score_value > linter.config.fail_under: sys.exit(0) sys.exit(self.linter.msg_status) diff --git a/tests/input/fail_under_minus6.py b/tests/regrtest_data/fail_under_minus6.py similarity index 100% rename from tests/input/fail_under_minus6.py rename to tests/regrtest_data/fail_under_minus6.py diff --git a/tests/input/fail_under_plus6.py b/tests/regrtest_data/fail_under_plus6.py similarity index 100% rename from tests/input/fail_under_plus6.py rename to tests/regrtest_data/fail_under_plus6.py diff --git a/tests/test_self.py b/tests/test_self.py index 0b325a6b0fb..359d33f5842 100644 --- a/tests/test_self.py +++ b/tests/test_self.py @@ -616,3 +616,51 @@ def check(lines): result = subprocess.check_output([sys.executable, "-m", "pylint", "--version"]) result = result.decode("utf-8") check(result.splitlines()) + + def test_fail_under(self): + self._runtest( + [ + "--fail-under", + "5", + "--enable=all", + join(HERE, "regrtest_data", "fail_under_plus6.py"), + ], + code=0, + ) + self._runtest( + [ + "--fail-under", + "6", + "--enable=all", + join(HERE, "regrtest_data", "fail_under_plus6.py"), + ], + code=0, + ) + self._runtest( + [ + "--fail-under", + "7", + "--enable=all", + join(HERE, "regrtest_data", "fail_under_plus6.py"), + ], + code=16, + ) + + self._runtest( + [ + "--fail-under", + "0", + "--enable=all", + join(HERE, "regrtest_data", "fail_under_minus6.py"), + ], + code=22, + ) + self._runtest( + [ + "--fail-under", + "-10", + "--enable=all", + join(HERE, "regrtest_data", "fail_under_plus6.py"), + ], + code=0, + ) diff --git a/tests/unittest_fail_threshold.py b/tests/unittest_fail_threshold.py deleted file mode 100644 index 89a9fa866fc..00000000000 --- a/tests/unittest_fail_threshold.py +++ /dev/null @@ -1,20 +0,0 @@ -import sys -from os.path import abspath, dirname, join - -from pylint.lint import Run - -HERE = abspath(dirname(__file__)) - - -def test_fail_under_plus(): - try: - run = Run(["--fail-under", "-1", join(HERE, "input", "fail_under_plus6.py")]) - except SystemExit as ex: - assert ex.code == 0 - - -def test_fail_under_minus(): - try: - run = Run(["--fail-under", "-1", join(HERE, "input", "fail_under_minus6.py")]) - except SystemExit as ex: - assert ex.code != 0 From 114298dccd13e72fe4733034e07ae0f2bf3aaeda Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Thu, 21 Nov 2019 13:24:54 +0100 Subject: [PATCH 002/240] Remove unintentional debug statement --- pylint/lint.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pylint/lint.py b/pylint/lint.py index 6cc0c0a8924..c85d512ecc6 100644 --- a/pylint/lint.py +++ b/pylint/lint.py @@ -1774,7 +1774,6 @@ def __init__(self, args, reporter=None, do_exit=True): if linter.config.exit_zero: sys.exit(0) else: - print(score_value, linter.config.fail_under) if score_value and score_value > linter.config.fail_under: sys.exit(0) sys.exit(self.linter.msg_status) From 01dfa522195d79217c43065d3d013e2ee31d47b7 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Wed, 27 Nov 2019 09:41:04 +0100 Subject: [PATCH 003/240] ``safe_infer`` can infer a value as long as all the paths share the same type. This permits finding more errors for functions whose return values use the same type. Until now we were stopping after the first inference result if we detected that a node value had multiple possible results. Close #2503 --- ChangeLog | 4 +++ pylint/checkers/utils.py | 27 +++++++++++++++----- tests/functional/s/string_formatting.py | 1 - tests/functional/s/super_checks.py | 2 +- tests/functional/u/unsubscriptable_value.py | 13 ++++++++++ tests/functional/u/unsubscriptable_value.txt | 1 + 6 files changed, 40 insertions(+), 8 deletions(-) diff --git a/ChangeLog b/ChangeLog index e90c5e23046..d18e16d664d 100644 --- a/ChangeLog +++ b/ChangeLog @@ -7,6 +7,10 @@ What's New in Pylint 2.5.0? Release date: TBA +* ``safe_infer`` can infer a value as long as all the paths share the same type. + + Close #2503 + * Add a --fail-under flag, also configurable in a .pylintrc file. If the final score is more than the specified score, it's considered a success and pylint exits with exitcode 0. Otherwise, it's considered a failure and pylint exits with its current exitcode based on the messages issued. Close #2242 diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index 4bda9ff789f..41b3876cbb3 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -1075,6 +1075,13 @@ def supports_delitem(value: astroid.node_classes.NodeNG) -> bool: return _supports_protocol(value, _supports_delitem_protocol) +def _get_python_type_of_node(node): + pytype = getattr(node, "pytype", None) + if callable(pytype): + return pytype() + return None + + @lru_cache(maxsize=1024) def safe_infer( node: astroid.node_classes.NodeNG, context=None @@ -1082,20 +1089,28 @@ def safe_infer( """Return the inferred value for the given node. Return None if inference failed or if there is some ambiguity (more than - one node has been inferred). + one node has been inferred of different types). """ + inferred_types = set() try: - inferit = node.infer(context=context) - value = next(inferit) + infer_gen = node.infer(context=context) + value = next(infer_gen) except astroid.InferenceError: return None + + if value is not astroid.Uninferable: + inferred_types.add(_get_python_type_of_node(value)) + try: - next(inferit) - return None # None if there is ambiguity on the inferred node + for inferred in infer_gen: + inferred_type = _get_python_type_of_node(inferred) + if inferred_type not in inferred_types: + return None # If there is ambiguity on the inferred node. except astroid.InferenceError: - return None # there is some kind of ambiguity + return None # There is some kind of ambiguity except StopIteration: return value + return value if len(inferred_types) <= 1 else None def has_known_bases(klass: astroid.ClassDef, context=None) -> bool: diff --git a/tests/functional/s/string_formatting.py b/tests/functional/s/string_formatting.py index bb7166fdebf..8e49d28b4bd 100644 --- a/tests/functional/s/string_formatting.py +++ b/tests/functional/s/string_formatting.py @@ -190,7 +190,6 @@ def avoid_empty_attribute(): def invalid_format_index_on_inference_ambiguity(): """Test inference bug for invalid-format-index""" - options = [] if len(sys.argv) > 1: options = [["Woof!"]] else: diff --git a/tests/functional/s/super_checks.py b/tests/functional/s/super_checks.py index 3888488fe4b..241531b374d 100644 --- a/tests/functional/s/super_checks.py +++ b/tests/functional/s/super_checks.py @@ -111,7 +111,7 @@ class TimeoutExpired(subprocess.CalledProcessError): def __init__(self): returncode = -1 self.timeout = -1 - super(TimeoutExpired, self).__init__(returncode) + super(TimeoutExpired, self).__init__("", returncode) class SuperWithType(object): diff --git a/tests/functional/u/unsubscriptable_value.py b/tests/functional/u/unsubscriptable_value.py index 68280c86768..73c3ac40e98 100644 --- a/tests/functional/u/unsubscriptable_value.py +++ b/tests/functional/u/unsubscriptable_value.py @@ -112,3 +112,16 @@ def __init__(self): def test_unsubscriptable(self): self.bala[0] self.portocala[0] + + +def return_an_int(param): + """Returns an int""" + if param == 0: + return 1 + return 0 + + +def test_one(param): + """Should complain about var_one[0], but doesn't""" + var_one = return_an_int(param) + return var_one[0] # [unsubscriptable-object] diff --git a/tests/functional/u/unsubscriptable_value.txt b/tests/functional/u/unsubscriptable_value.txt index d3243946dff..b29b212dbcc 100644 --- a/tests/functional/u/unsubscriptable_value.txt +++ b/tests/functional/u/unsubscriptable_value.txt @@ -12,3 +12,4 @@ unsubscriptable-object:56::Value 'set(numbers)' is unsubscriptable unsubscriptable-object:57::Value 'frozenset(numbers)' is unsubscriptable unsubscriptable-object:77::Value 'SubscriptableClass()' is unsubscriptable unsubscriptable-object:84::Value 'test' is unsubscriptable +unsubscriptable-object:127:test_one:Value 'var_one' is unsubscriptable From c6322018ab54b082d42b0da1809951e703685ddf Mon Sep 17 00:00:00 2001 From: craig-sh Date: Wed, 27 Nov 2019 10:00:08 -0500 Subject: [PATCH 004/240] Enhance the protocol checker (#3259) This commit adds multiple checks for various Python protocols E0304 (invalid-bool-returned): __bool__ did not return a bool E0305 (invalid-index-returned): __index__ did not return an integer E0306 (invalid-repr-returned): __repr__ did not return a string E0307 (invalid-str-returned): __str__ did not return a string E0308 (invalid-bytes-returned): __bytes__ did not return a string E0309 (invalid-hash-returned): __hash__ did not return an integer E0310 (invalid-length-hint-returned): __length_hint__ did not return a non-negative integer E0311 (invalid-format-returned): __format__ did not return a string E0312 (invalid-getnewargs-returned): __getnewargs__ did not return a tuple E0313 (invalid-getnewargs-ex-returned): __getnewargs_ex__ did not return a tuple of the form (tuple, dict) Close #560 --- CONTRIBUTORS.txt | 2 + ChangeLog | 14 ++ doc/whatsnew/2.5.rst | 12 + pylint/checkers/classes.py | 234 ++++++++++++++++-- tests/functional/i/invalid_bool_returned.py | 62 +++++ tests/functional/i/invalid_bool_returned.txt | 3 + tests/functional/i/invalid_bytes_returned.py | 64 +++++ tests/functional/i/invalid_bytes_returned.txt | 3 + tests/functional/i/invalid_format_returned.py | 64 +++++ .../functional/i/invalid_format_returned.txt | 3 + .../i/invalid_getnewargs_ex_returned.py | 85 +++++++ .../i/invalid_getnewargs_ex_returned.txt | 6 + .../i/invalid_getnewargs_returned.py | 62 +++++ .../i/invalid_getnewargs_returned.txt | 3 + tests/functional/i/invalid_hash_returned.py | 71 ++++++ tests/functional/i/invalid_hash_returned.txt | 4 + tests/functional/i/invalid_index_returned.py | 71 ++++++ tests/functional/i/invalid_index_returned.txt | 4 + .../i/invalid_length_hint_returned.py | 64 +++++ .../i/invalid_length_hint_returned.txt | 3 + tests/functional/i/invalid_repr_returned.py | 64 +++++ tests/functional/i/invalid_repr_returned.txt | 3 + tests/functional/i/invalid_str_returned.py | 64 +++++ tests/functional/i/invalid_str_returned.txt | 3 + 24 files changed, 942 insertions(+), 26 deletions(-) create mode 100644 tests/functional/i/invalid_bool_returned.py create mode 100644 tests/functional/i/invalid_bool_returned.txt create mode 100644 tests/functional/i/invalid_bytes_returned.py create mode 100644 tests/functional/i/invalid_bytes_returned.txt create mode 100644 tests/functional/i/invalid_format_returned.py create mode 100644 tests/functional/i/invalid_format_returned.txt create mode 100644 tests/functional/i/invalid_getnewargs_ex_returned.py create mode 100644 tests/functional/i/invalid_getnewargs_ex_returned.txt create mode 100644 tests/functional/i/invalid_getnewargs_returned.py create mode 100644 tests/functional/i/invalid_getnewargs_returned.txt create mode 100644 tests/functional/i/invalid_hash_returned.py create mode 100644 tests/functional/i/invalid_hash_returned.txt create mode 100644 tests/functional/i/invalid_index_returned.py create mode 100644 tests/functional/i/invalid_index_returned.txt create mode 100644 tests/functional/i/invalid_length_hint_returned.py create mode 100644 tests/functional/i/invalid_length_hint_returned.txt create mode 100644 tests/functional/i/invalid_repr_returned.py create mode 100644 tests/functional/i/invalid_repr_returned.txt create mode 100644 tests/functional/i/invalid_str_returned.py create mode 100644 tests/functional/i/invalid_str_returned.txt diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 606d4d0df77..48925317a34 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -352,3 +352,5 @@ contributors: * Bastien Vallet: contributor * Pek Chhan: contributor + +* Craig Henriques: contributor diff --git a/ChangeLog b/ChangeLog index d18e16d664d..314d041a18e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -7,6 +7,20 @@ What's New in Pylint 2.5.0? Release date: TBA +* Added errors for protocol functions when invalid return types are detected. + E0304 (invalid-bool-returned): __bool__ did not return a bool + E0305 (invalid-index-returned): __index__ did not return an integer + E0306 (invalid-repr-returned): __repr__ did not return a string + E0307 (invalid-str-returned): __str__ did not return a string + E0308 (invalid-bytes-returned): __bytes__ did not return a string + E0309 (invalid-hash-returned): __hash__ did not return an integer + E0310 (invalid-length-hint-returned): __length_hint__ did not return a non-negative integer + E0311 (invalid-format-returned): __format__ did not return a string + E0312 (invalid-getnewargs-returned): __getnewargs__ did not return a tuple + E0313 (invalid-getnewargs-ex-returned): __getnewargs_ex__ did not return a tuple of the form (tuple, dict) + + Close #560 + * ``safe_infer`` can infer a value as long as all the paths share the same type. Close #2503 diff --git a/doc/whatsnew/2.5.rst b/doc/whatsnew/2.5.rst index a764799ce5e..6586c5a01d1 100644 --- a/doc/whatsnew/2.5.rst +++ b/doc/whatsnew/2.5.rst @@ -19,6 +19,18 @@ New checkers f-string without having any interpolated values in it, which means that the f-string can be a normal string. +* Checks for invalid return types from protocol functions: + + * ``__bool__`` must return a bool + * ``__index__`` must return an integer + * ``__repr__`` must return a string + * ``__str__`` must return a string + * ``__bytes__`` must return bytes + * ``__hash__`` must return a string + * ``__length__hint__`` must return a non-negative integer + * ``__format__`` must return a string + * ``__getnewargs__`` must return a tuple + * ``__getnewargs_ex__`` must return a tuple of the form (tuple, dict) Other Changes ============= diff --git a/pylint/checkers/classes.py b/pylint/checkers/classes.py index 8a5f0e0fbaa..b8bcf9b1846 100644 --- a/pylint/checkers/classes.py +++ b/pylint/checkers/classes.py @@ -1719,23 +1719,105 @@ class SpecialMethodsChecker(BaseChecker): "invalid-length-returned", "Used when a __len__ method returns something which is not a " "non-negative integer", - {}, + ), + "E0304": ( + "__bool__ does not return bool", + "invalid-bool-returned", + "Used when a __bool__ method returns something which is not a bool", + ), + "E0305": ( + "__index__ does not return int", + "invalid-index-returned", + "Used when an __index__ method returns something which is not " + "an integer", + ), + "E0306": ( + "__repr__ does not return str", + "invalid-repr-returned", + "Used when a __repr__ method returns something which is not a string", + ), + "E0307": ( + "__str__ does not return str", + "invalid-str-returned", + "Used when a __str__ method returns something which is not a string", + ), + "E0308": ( + "__bytes__ does not return bytes", + "invalid-bytes-returned", + "Used when a __bytes__ method returns something which is not bytes", + ), + "E0309": ( + "__hash__ does not return int", + "invalid-hash-returned", + "Used when a __hash__ method returns something which is not an integer", + ), + "E0310": ( + "__length_hint__ does not return non-negative integer", + "invalid-length-hint-returned", + "Used when a __length_hint__ method returns something which is not a " + "non-negative integer", + ), + "E0311": ( + "__format__ does not return str", + "invalid-format-returned", + "Used when a __format__ method returns something which is not a string", + ), + "E0312": ( + "__getnewargs__ does not return a tuple", + "invalid-getnewargs-returned", + "Used when a __getnewargs__ method returns something which is not " + "a tuple", + ), + "E0313": ( + "__getnewargs_ex__ does not return a tuple containing (tuple, dict)", + "invalid-getnewargs-ex-returned", + "Used when a __getnewargs_ex__ method returns something which is not " + "of the form tuple(tuple, dict)", ), } priority = -2 + def __init__(self, linter=None): + BaseChecker.__init__(self, linter) + self._protocol_map = { + "__iter__": self._check_iter, + "__len__": self._check_len, + "__bool__": self._check_bool, + "__index__": self._check_index, + "__repr__": self._check_repr, + "__str__": self._check_str, + "__bytes__": self._check_bytes, + "__hash__": self._check_hash, + "__length_hint__": self._check_length_hint, + "__format__": self._check_format, + "__getnewargs__": self._check_getnewargs, + "__getnewargs_ex__": self._check_getnewargs_ex, + } + @check_messages( "unexpected-special-method-signature", "non-iterator-returned", "invalid-length-returned", + "invalid-bool-returned", + "invalid-index-returned", + "invalid-repr-returned", + "invalid-str-returned", + "invalid-bytes-returned", + "invalid-hash-returned", + "invalid-length-hint-returned", + "invalid-format-returned", + "invalid-getnewargs-returned", + "invalid-getnewargs-ex-returned", ) def visit_functiondef(self, node): if not node.is_method(): return - if node.name == "__iter__": - self._check_iter(node) - if node.name == "__len__": - self._check_len(node) + + inferred = _safe_infer_call_result(node, node) + # Only want to check types that we are able to infer + if inferred and node.name in self._protocol_map: + self._protocol_map[node.name](node, inferred) + if node.name in PYMETHODS: self._check_unexpected_method_signature(node) @@ -1788,6 +1870,56 @@ def _check_unexpected_method_signature(self, node): node=node, ) + @staticmethod + def _is_wrapped_type(node, type_): + return ( + isinstance(node, astroid.Instance) + and node.name == type_ + and not isinstance(node, astroid.Const) + ) + + @staticmethod + def _is_int(node): + if SpecialMethodsChecker._is_wrapped_type(node, "int"): + return True + + return isinstance(node, astroid.Const) and isinstance(node.value, int) + + @staticmethod + def _is_str(node): + if SpecialMethodsChecker._is_wrapped_type(node, "str"): + return True + + return isinstance(node, astroid.Const) and isinstance(node.value, str) + + @staticmethod + def _is_bool(node): + if SpecialMethodsChecker._is_wrapped_type(node, "bool"): + return True + + return isinstance(node, astroid.Const) and isinstance(node.value, bool) + + @staticmethod + def _is_bytes(node): + if SpecialMethodsChecker._is_wrapped_type(node, "bytes"): + return True + + return isinstance(node, astroid.Const) and isinstance(node.value, bytes) + + @staticmethod + def _is_tuple(node): + if SpecialMethodsChecker._is_wrapped_type(node, "tuple"): + return True + + return isinstance(node, astroid.Const) and isinstance(node.value, tuple) + + @staticmethod + def _is_dict(node): + if SpecialMethodsChecker._is_wrapped_type(node, "dict"): + return True + + return isinstance(node, astroid.Const) and isinstance(node.value, dict) + @staticmethod def _is_iterator(node): if node is astroid.Uninferable: @@ -1813,33 +1945,83 @@ def _is_iterator(node): pass return False - def _check_iter(self, node): - inferred = _safe_infer_call_result(node, node) - if inferred is not None: - if not self._is_iterator(inferred): - self.add_message("non-iterator-returned", node=node) + def _check_iter(self, node, inferred): + if not self._is_iterator(inferred): + self.add_message("non-iterator-returned", node=node) - def _check_len(self, node): - inferred = _safe_infer_call_result(node, node) - if not inferred or inferred is astroid.Uninferable: - return + def _check_len(self, node, inferred): + if not self._is_int(inferred): + self.add_message("invalid-length-returned", node=node) + elif isinstance(inferred, astroid.Const) and inferred.value < 0: + self.add_message("invalid-length-returned", node=node) - if ( - isinstance(inferred, astroid.Instance) - and inferred.name == "int" - and not isinstance(inferred, astroid.Const) - ): - # Assume it's good enough, since the int() call might wrap - # something that's uninferable for us + def _check_bool(self, node, inferred): + if not self._is_bool(inferred): + self.add_message("invalid-bool-returned", node=node) + + def _check_index(self, node, inferred): + if not self._is_int(inferred): + self.add_message("invalid-index-returned", node=node) + + def _check_repr(self, node, inferred): + if not self._is_str(inferred): + self.add_message("invalid-repr-returned", node=node) + + def _check_str(self, node, inferred): + if not self._is_str(inferred): + self.add_message("invalid-str-returned", node=node) + + def _check_bytes(self, node, inferred): + if not self._is_bytes(inferred): + self.add_message("invalid-bytes-returned", node=node) + + def _check_hash(self, node, inferred): + if not self._is_int(inferred): + self.add_message("invalid-hash-returned", node=node) + + def _check_length_hint(self, node, inferred): + if not self._is_int(inferred): + self.add_message("invalid-length-hint-returned", node=node) + elif isinstance(inferred, astroid.Const) and inferred.value < 0: + self.add_message("invalid-length-hint-returned", node=node) + + def _check_format(self, node, inferred): + if not self._is_str(inferred): + self.add_message("invalid-format-returned", node=node) + + def _check_getnewargs(self, node, inferred): + if not self._is_tuple(inferred): + self.add_message("invalid-getnewargs-returned", node=node) + + def _check_getnewargs_ex(self, node, inferred): + if not self._is_tuple(inferred): + self.add_message("invalid-getnewargs-ex-returned", node=node) return - if not isinstance(inferred, astroid.Const): - self.add_message("invalid-length-returned", node=node) + if not isinstance(inferred, astroid.Tuple): + # If it's not an astroid.Tuple we can't analyze it further return - value = inferred.value - if not isinstance(value, int) or value < 0: - self.add_message("invalid-length-returned", node=node) + found_error = False + + if len(inferred.elts) != 2: + found_error = True + else: + for arg, check in [ + (inferred.elts[0], self._is_tuple), + (inferred.elts[1], self._is_dict), + ]: + + if isinstance(arg, astroid.Call): + arg = safe_infer(arg) + + if arg and arg is not astroid.Uninferable: + if not check(arg): + found_error = True + break + + if found_error: + self.add_message("invalid-getnewargs-ex-returned", node=node) def _ancestors_to_call(klass_node, method="__init__"): diff --git a/tests/functional/i/invalid_bool_returned.py b/tests/functional/i/invalid_bool_returned.py new file mode 100644 index 00000000000..0e68ed2636e --- /dev/null +++ b/tests/functional/i/invalid_bool_returned.py @@ -0,0 +1,62 @@ +"""Check invalid value returned by __bool__ """ + +# pylint: disable=too-few-public-methods,missing-docstring,no-self-use,import-error, useless-object-inheritance +import six + +from missing import Missing + + +class FirstGoodBool(object): + """__bool__ returns """ + + def __bool__(self): + return True + + +class SecondGoodBool(object): + """__bool__ returns """ + + def __bool__(self): + return bool(0) + + +class BoolMetaclass(type): + def __bool__(cls): + return True + + +@six.add_metaclass(BoolMetaclass) +class ThirdGoodBool(object): + """Bool through the metaclass.""" + + +class FirstBadBool(object): + """ __bool__ returns an integer """ + + def __bool__(self): # [invalid-bool-returned] + return 1 + + +class SecondBadBool(object): + """ __bool__ returns str """ + + def __bool__(self): # [invalid-bool-returned] + return "True" + + +class ThirdBadBool(object): + """ __bool__ returns node which does not have 'value' in AST """ + + def __bool__(self): # [invalid-bool-returned] + return lambda: 3 + + +class AmbigousBool(object): + """ Uninferable return value """ + __bool__ = lambda self: Missing + + +class AnotherAmbiguousBool(object): + """Potential uninferable return value""" + def __bool__(self): + return bool(Missing) diff --git a/tests/functional/i/invalid_bool_returned.txt b/tests/functional/i/invalid_bool_returned.txt new file mode 100644 index 00000000000..e33fb40e1b7 --- /dev/null +++ b/tests/functional/i/invalid_bool_returned.txt @@ -0,0 +1,3 @@ +invalid-bool-returned:36:FirstBadBool.__bool__:__bool__ does not return bool +invalid-bool-returned:43:SecondBadBool.__bool__:__bool__ does not return bool +invalid-bool-returned:50:ThirdBadBool.__bool__:__bool__ does not return bool diff --git a/tests/functional/i/invalid_bytes_returned.py b/tests/functional/i/invalid_bytes_returned.py new file mode 100644 index 00000000000..703fea27836 --- /dev/null +++ b/tests/functional/i/invalid_bytes_returned.py @@ -0,0 +1,64 @@ +"""Check invalid value returned by __bytes__ """ + +# pylint: disable=too-few-public-methods,missing-docstring,no-self-use,import-error, useless-object-inheritance +import six + +from missing import Missing + + +class FirstGoodBytes(object): + """__bytes__ returns """ + + def __bytes__(self): + return b"some bytes" + + +class SecondGoodBytes(object): + """__bytes__ returns """ + + def __bytes__(self): + return bytes("123", "ascii") + + +class BytesMetaclass(type): + def __bytes__(cls): + return b"some bytes" + + +@six.add_metaclass(BytesMetaclass) +class ThirdGoodBytes(object): + """Bytes through the metaclass.""" + + +class FirstBadBytes(object): + """ __bytes__ returns bytes """ + + def __bytes__(self): # [invalid-bytes-returned] + return "123" + + +class SecondBadBytes(object): + """ __bytes__ returns int """ + + def __bytes__(self): # [invalid-bytes-returned] + return 1 + + +class ThirdBadBytes(object): + """ __bytes__ returns node which does not have 'value' in AST """ + + def __bytes__(self): # [invalid-bytes-returned] + return lambda: b"some bytes" + + +class AmbiguousBytes(object): + """ Uninferable return value """ + + __bytes__ = lambda self: Missing + + +class AnotherAmbiguousBytes(object): + """Potential uninferable return value""" + + def __bytes__(self): + return bytes(Missing) diff --git a/tests/functional/i/invalid_bytes_returned.txt b/tests/functional/i/invalid_bytes_returned.txt new file mode 100644 index 00000000000..9f643d5721c --- /dev/null +++ b/tests/functional/i/invalid_bytes_returned.txt @@ -0,0 +1,3 @@ +invalid-bytes-returned:36:FirstBadBytes.__bytes__:__bytes__ does not return bytes +invalid-bytes-returned:43:SecondBadBytes.__bytes__:__bytes__ does not return bytes +invalid-bytes-returned:50:ThirdBadBytes.__bytes__:__bytes__ does not return bytes diff --git a/tests/functional/i/invalid_format_returned.py b/tests/functional/i/invalid_format_returned.py new file mode 100644 index 00000000000..ec428b4b31d --- /dev/null +++ b/tests/functional/i/invalid_format_returned.py @@ -0,0 +1,64 @@ +"""Check invalid value returned by __format__ """ + +# pylint: disable=too-few-public-methods,missing-docstring,no-self-use,import-error, useless-object-inheritance +import six + +from missing import Missing + + +class FirstGoodFormat(object): + """__format__ returns """ + + def __format__(self, format_spec): + return "some format" + + +class SecondGoodFormat(object): + """__format__ returns """ + + def __format__(self, format_spec): + return str(123) + + +class FormatMetaclass(type): + def __format__(cls, format_spec): + return "some format" + + +@six.add_metaclass(FormatMetaclass) +class ThirdGoodFormat(object): + """Format through the metaclass.""" + + +class FirstBadFormat(object): + """ __format__ returns bytes """ + + def __format__(self, format_spec): # [invalid-format-returned] + return b"123" + + +class SecondBadFormat(object): + """ __format__ returns int """ + + def __format__(self, format_spec): # [invalid-format-returned] + return 1 + + +class ThirdBadFormat(object): + """ __format__ returns node which does not have 'value' in AST """ + + def __format__(self, format_spec): # [invalid-format-returned] + return lambda: "some format" + + +class AmbiguousFormat(object): + """ Uninferable return value """ + + __format__ = lambda self, format_spec: Missing + + +class AnotherAmbiguousFormat(object): + """Potential uninferable return value""" + + def __format__(self, format_spec): + return str(Missing) diff --git a/tests/functional/i/invalid_format_returned.txt b/tests/functional/i/invalid_format_returned.txt new file mode 100644 index 00000000000..96186c2cb8c --- /dev/null +++ b/tests/functional/i/invalid_format_returned.txt @@ -0,0 +1,3 @@ +invalid-format-returned:36:FirstBadFormat.__format__:__format__ does not return str +invalid-format-returned:43:SecondBadFormat.__format__:__format__ does not return str +invalid-format-returned:50:ThirdBadFormat.__format__:__format__ does not return str diff --git a/tests/functional/i/invalid_getnewargs_ex_returned.py b/tests/functional/i/invalid_getnewargs_ex_returned.py new file mode 100644 index 00000000000..e3789509622 --- /dev/null +++ b/tests/functional/i/invalid_getnewargs_ex_returned.py @@ -0,0 +1,85 @@ +"""Check invalid value returned by __getnewargs_ex__ """ + +# pylint: disable=too-few-public-methods,missing-docstring,no-self-use,import-error, useless-object-inheritance +import six + +from missing import Missing + + +class FirstGoodGetNewArgsEx(object): + """__getnewargs_ex__ returns """ + + def __getnewargs_ex__(self): + return ((1,), {"2": "2"}) + + +class SecondGoodGetNewArgsEx(object): + """__getnewargs_ex__ returns """ + + def __getnewargs_ex__(self): + return (tuple(), dict()) + + +class GetNewArgsExMetaclass(type): + def __getnewargs_ex__(cls): + return ((1,), {"2": "2"}) + + +@six.add_metaclass(GetNewArgsExMetaclass) +class ThirdGoodGetNewArgsEx(object): + """GetNewArgsEx through the metaclass.""" + + +class FirstBadGetNewArgsEx(object): + """ __getnewargs_ex__ returns an integer """ + + def __getnewargs_ex__(self): # [invalid-getnewargs-ex-returned] + return 1 + + +class SecondBadGetNewArgsEx(object): + """ __getnewargs_ex__ returns tuple with incorrect arg length""" + + def __getnewargs_ex__(self): # [invalid-getnewargs-ex-returned] + return (tuple(1), dict(x="y"), 1) + + +class ThirdBadGetNewArgsEx(object): + """ __getnewargs_ex__ returns tuple with wrong type for first arg """ + + def __getnewargs_ex__(self): # [invalid-getnewargs-ex-returned] + return (dict(x="y"), dict(x="y")) + + +class FourthBadGetNewArgsEx(object): + """ __getnewargs_ex__ returns tuple with wrong type for second arg """ + + def __getnewargs_ex__(self): # [invalid-getnewargs-ex-returned] + return ((1, ), (1, )) + + +class FifthBadGetNewArgsEx(object): + """ __getnewargs_ex__ returns tuple with wrong type for both args """ + + def __getnewargs_ex__(self): # [invalid-getnewargs-ex-returned] + return ({'x': 'y'}, (2,)) + + +class SixthBadGetNewArgsEx(object): + """ __getnewargs_ex__ returns node which does not have 'value' in AST """ + + def __getnewargs_ex__(self): # [invalid-getnewargs-ex-returned] + return lambda: (1, 2) + + +class AmbigousGetNewArgsEx(object): + """ Uninferable return value """ + + __getnewargs_ex__ = lambda self: Missing + + +class AnotherAmbiguousGetNewArgsEx(object): + """Potential uninferable return value""" + + def __getnewargs_ex__(self): + return tuple(Missing) diff --git a/tests/functional/i/invalid_getnewargs_ex_returned.txt b/tests/functional/i/invalid_getnewargs_ex_returned.txt new file mode 100644 index 00000000000..cf549e6fd72 --- /dev/null +++ b/tests/functional/i/invalid_getnewargs_ex_returned.txt @@ -0,0 +1,6 @@ +invalid-getnewargs-ex-returned:36:FirstBadGetNewArgsEx.__getnewargs_ex__:__getnewargs_ex__ does not return a tuple containing (tuple, dict) +invalid-getnewargs-ex-returned:43:SecondBadGetNewArgsEx.__getnewargs_ex__:__getnewargs_ex__ does not return a tuple containing (tuple, dict) +invalid-getnewargs-ex-returned:50:ThirdBadGetNewArgsEx.__getnewargs_ex__:__getnewargs_ex__ does not return a tuple containing (tuple, dict) +invalid-getnewargs-ex-returned:57:FourthBadGetNewArgsEx.__getnewargs_ex__:__getnewargs_ex__ does not return a tuple containing (tuple, dict) +invalid-getnewargs-ex-returned:64:FifthBadGetNewArgsEx.__getnewargs_ex__:__getnewargs_ex__ does not return a tuple containing (tuple, dict) +invalid-getnewargs-ex-returned:71:SixthBadGetNewArgsEx.__getnewargs_ex__:__getnewargs_ex__ does not return a tuple containing (tuple, dict) diff --git a/tests/functional/i/invalid_getnewargs_returned.py b/tests/functional/i/invalid_getnewargs_returned.py new file mode 100644 index 00000000000..ee5305fe873 --- /dev/null +++ b/tests/functional/i/invalid_getnewargs_returned.py @@ -0,0 +1,62 @@ +"""Check invalid value returned by __getnewargs__ """ + +# pylint: disable=too-few-public-methods,missing-docstring,no-self-use,import-error, useless-object-inheritance +import six + +from missing import Missing + + +class FirstGoodGetNewArgs(object): + """__getnewargs__ returns """ + + def __getnewargs__(self): + return (1, "2", 3) + + +class SecondGoodGetNewArgs(object): + """__getnewargs__ returns """ + + def __getnewargs__(self): + return tuple() + + +class GetNewArgsMetaclass(type): + def __getnewargs__(cls): + return (1, 2, 3) + + +@six.add_metaclass(GetNewArgsMetaclass) +class ThirdGoodGetNewArgs(object): + """GetNewArgs through the metaclass.""" + + +class FirstBadGetNewArgs(object): + """ __getnewargs__ returns an integer """ + + def __getnewargs__(self): # [invalid-getnewargs-returned] + return 1 + + +class SecondBadGetNewArgs(object): + """ __getnewargs__ returns str """ + + def __getnewargs__(self): # [invalid-getnewargs-returned] + return "(1, 2, 3)" + + +class ThirdBadGetNewArgs(object): + """ __getnewargs__ returns node which does not have 'value' in AST """ + + def __getnewargs__(self): # [invalid-getnewargs-returned] + return lambda: tuple(1, 2) + + +class AmbigousGetNewArgs(object): + """ Uninferable return value """ + __getnewargs__ = lambda self: Missing + + +class AnotherAmbiguousGetNewArgs(object): + """Potential uninferable return value""" + def __getnewargs__(self): + return tuple(Missing) diff --git a/tests/functional/i/invalid_getnewargs_returned.txt b/tests/functional/i/invalid_getnewargs_returned.txt new file mode 100644 index 00000000000..e1ac0a30e59 --- /dev/null +++ b/tests/functional/i/invalid_getnewargs_returned.txt @@ -0,0 +1,3 @@ +invalid-getnewargs-returned:36:FirstBadGetNewArgs.__getnewargs__:__getnewargs__ does not return a tuple +invalid-getnewargs-returned:43:SecondBadGetNewArgs.__getnewargs__:__getnewargs__ does not return a tuple +invalid-getnewargs-returned:50:ThirdBadGetNewArgs.__getnewargs__:__getnewargs__ does not return a tuple diff --git a/tests/functional/i/invalid_hash_returned.py b/tests/functional/i/invalid_hash_returned.py new file mode 100644 index 00000000000..47f63e99b12 --- /dev/null +++ b/tests/functional/i/invalid_hash_returned.py @@ -0,0 +1,71 @@ +"""Check invalid value returned by __hash__ """ + +# pylint: disable=too-few-public-methods,missing-docstring,no-self-use,import-error, useless-object-inheritance +import six + +from missing import Missing + + +class FirstGoodHash(object): + """__hash__ returns """ + + def __hash__(self): + return 1 + + +class SecondGoodHash(object): + """__hash__ returns """ + + def __hash__(self): + return 0 + + +class HashMetaclass(type): + def __hash__(cls): + return 1 + + +@six.add_metaclass(HashMetaclass) +class ThirdGoodHash(object): + """Hash through the metaclass.""" + + +class FirstBadHash(object): + """ __hash__ returns a dict """ + + def __hash__(self): # [invalid-hash-returned] + return {} + + +class SecondBadHash(object): + """ __hash__ returns str """ + + def __hash__(self): # [invalid-hash-returned] + return "True" + + +class ThirdBadHash(object): + """ __hash__ returns a float""" + + def __hash__(self): # [invalid-hash-returned] + return 1.11 + + +class FourthBadHash(object): + """ __hash__ returns node which does not have 'value' in AST """ + + def __hash__(self): # [invalid-hash-returned] + return lambda: 3 + + +class AmbigousHash(object): + """ Uninferable return value """ + + __hash__ = lambda self: Missing + + +class AnotherAmbiguousHash(object): + """Potential uninferable return value""" + + def __hash__(self): + return hash(Missing) diff --git a/tests/functional/i/invalid_hash_returned.txt b/tests/functional/i/invalid_hash_returned.txt new file mode 100644 index 00000000000..a0d09361687 --- /dev/null +++ b/tests/functional/i/invalid_hash_returned.txt @@ -0,0 +1,4 @@ +invalid-hash-returned:36:FirstBadHash.__hash__:__hash__ does not return int +invalid-hash-returned:43:SecondBadHash.__hash__:__hash__ does not return int +invalid-hash-returned:50:ThirdBadHash.__hash__:__hash__ does not return int +invalid-hash-returned:57:FourthBadHash.__hash__:__hash__ does not return int diff --git a/tests/functional/i/invalid_index_returned.py b/tests/functional/i/invalid_index_returned.py new file mode 100644 index 00000000000..c48d9a837b3 --- /dev/null +++ b/tests/functional/i/invalid_index_returned.py @@ -0,0 +1,71 @@ +"""Check invalid value returned by __index__ """ + +# pylint: disable=too-few-public-methods,missing-docstring,no-self-use,import-error, useless-object-inheritance +import six + +from missing import Missing + + +class FirstGoodIndex(object): + """__index__ returns """ + + def __index__(self): + return 1 + + +class SecondGoodIndex(object): + """__index__ returns """ + + def __index__(self): + return 0 + + +class IndexMetaclass(type): + def __index__(cls): + return 1 + + +@six.add_metaclass(IndexMetaclass) +class ThirdGoodIndex(object): + """Index through the metaclass.""" + + +class FirstBadIndex(object): + """ __index__ returns a dict """ + + def __index__(self): # [invalid-index-returned] + return {'1': '1'} + + +class SecondBadIndex(object): + """ __index__ returns str """ + + def __index__(self): # [invalid-index-returned] + return "42" + + +class ThirdBadIndex(object): + """ __index__ returns a float""" + + def __index__(self): # [invalid-index-returned] + return 1.11 + + +class FourthBadIndex(object): + """ __index__ returns node which does not have 'value' in AST """ + + def __index__(self): # [invalid-index-returned] + return lambda: 3 + + +class AmbigousIndex(object): + """ Uninferable return value """ + + __index__ = lambda self: Missing + + +class AnotherAmbiguousIndex(object): + """Potential uninferable return value""" + + def __index__(self): + return int(Missing) diff --git a/tests/functional/i/invalid_index_returned.txt b/tests/functional/i/invalid_index_returned.txt new file mode 100644 index 00000000000..fc7ba75f04a --- /dev/null +++ b/tests/functional/i/invalid_index_returned.txt @@ -0,0 +1,4 @@ +invalid-index-returned:36:FirstBadIndex.__index__:__index__ does not return int +invalid-index-returned:43:SecondBadIndex.__index__:__index__ does not return int +invalid-index-returned:50:ThirdBadIndex.__index__:__index__ does not return int +invalid-index-returned:57:FourthBadIndex.__index__:__index__ does not return int diff --git a/tests/functional/i/invalid_length_hint_returned.py b/tests/functional/i/invalid_length_hint_returned.py new file mode 100644 index 00000000000..7696bc9fdeb --- /dev/null +++ b/tests/functional/i/invalid_length_hint_returned.py @@ -0,0 +1,64 @@ +"""Check invalid value returned by __length_hint__ """ + +# pylint: disable=too-few-public-methods,missing-docstring,no-self-use,import-error, useless-object-inheritance +import sys + +import six + +from missing import Missing + + +class FirstGoodLengthHint(object): + """__length_hint__ returns """ + + def __length_hint__(self): + return 0 + + +class SecondGoodLengthHint(object): + """__length_hint__ returns """ + + def __length_hint__(self): + return sys.maxsize + 1 + + +class LengthHintMetaclass(type): + def __length_hint__(cls): + return 1 + + +@six.add_metaclass(LengthHintMetaclass) +class ThirdGoodLengthHint(object): + """LengthHintgth through the metaclass.""" + + +class FirstBadLengthHint(object): + """ __length_hint__ returns a negative integer """ + + def __length_hint__(self): # [invalid-length-hint-returned] + return -1 + + +class SecondBadLengthHint(object): + """ __length_hint__ returns non-int """ + + def __length_hint__(self): # [invalid-length-hint-returned] + return 3.0 + + +class ThirdBadLengthHint(object): + """ __length_hint__ returns node which does not have 'value' in AST """ + + def __length_hint__(self): # [invalid-length-hint-returned] + return lambda: 3 + + +class AmbigousLengthHint(object): + """ Uninferable return value """ + __length_hint__ = lambda self: Missing + + +class AnotherAmbiguousLengthHint(object): + """Potential uninferable return value""" + def __length_hint__(self): + return int(Missing) diff --git a/tests/functional/i/invalid_length_hint_returned.txt b/tests/functional/i/invalid_length_hint_returned.txt new file mode 100644 index 00000000000..325c736a662 --- /dev/null +++ b/tests/functional/i/invalid_length_hint_returned.txt @@ -0,0 +1,3 @@ +invalid-length-hint-returned:38:FirstBadLengthHint.__length_hint__:__length_hint__ does not return non-negative integer +invalid-length-hint-returned:45:SecondBadLengthHint.__length_hint__:__length_hint__ does not return non-negative integer +invalid-length-hint-returned:52:ThirdBadLengthHint.__length_hint__:__length_hint__ does not return non-negative integer diff --git a/tests/functional/i/invalid_repr_returned.py b/tests/functional/i/invalid_repr_returned.py new file mode 100644 index 00000000000..3bbd7828d81 --- /dev/null +++ b/tests/functional/i/invalid_repr_returned.py @@ -0,0 +1,64 @@ +"""Check invalid value returned by __repr__ """ + +# pylint: disable=too-few-public-methods,missing-docstring,no-self-use,import-error, useless-object-inheritance +import six + +from missing import Missing + + +class FirstGoodRepr(object): + """__repr__ returns """ + + def __repr__(self): + return "some repr" + + +class SecondGoodRepr(object): + """__repr__ returns """ + + def __repr__(self): + return str(123) + + +class ReprMetaclass(type): + def __repr__(cls): + return "some repr" + + +@six.add_metaclass(ReprMetaclass) +class ThirdGoodRepr(object): + """Repr through the metaclass.""" + + +class FirstBadRepr(object): + """ __repr__ returns bytes """ + + def __repr__(self): # [invalid-repr-returned] + return b"123" + + +class SecondBadRepr(object): + """ __repr__ returns int """ + + def __repr__(self): # [invalid-repr-returned] + return 1 + + +class ThirdBadRepr(object): + """ __repr__ returns node which does not have 'value' in AST """ + + def __repr__(self): # [invalid-repr-returned] + return lambda: "some repr" + + +class AmbiguousRepr(object): + """ Uninferable return value """ + + __repr__ = lambda self: Missing + + +class AnotherAmbiguousRepr(object): + """Potential uninferable return value""" + + def __repr__(self): + return str(Missing) diff --git a/tests/functional/i/invalid_repr_returned.txt b/tests/functional/i/invalid_repr_returned.txt new file mode 100644 index 00000000000..735cd341290 --- /dev/null +++ b/tests/functional/i/invalid_repr_returned.txt @@ -0,0 +1,3 @@ +invalid-repr-returned:36:FirstBadRepr.__repr__:__repr__ does not return str +invalid-repr-returned:43:SecondBadRepr.__repr__:__repr__ does not return str +invalid-repr-returned:50:ThirdBadRepr.__repr__:__repr__ does not return str diff --git a/tests/functional/i/invalid_str_returned.py b/tests/functional/i/invalid_str_returned.py new file mode 100644 index 00000000000..00ef2204619 --- /dev/null +++ b/tests/functional/i/invalid_str_returned.py @@ -0,0 +1,64 @@ +"""Check invalid value returned by __str__ """ + +# pylint: disable=too-few-public-methods,missing-docstring,no-self-use,import-error, useless-object-inheritance +import six + +from missing import Missing + + +class FirstGoodStr(object): + """__str__ returns """ + + def __str__(self): + return "some str" + + +class SecondGoodStr(object): + """__str__ returns """ + + def __str__(self): + return str(123) + + +class StrMetaclass(type): + def __str__(cls): + return "some str" + + +@six.add_metaclass(StrMetaclass) +class ThirdGoodStr(object): + """Str through the metaclass.""" + + +class FirstBadStr(object): + """ __str__ returns bytes """ + + def __str__(self): # [invalid-str-returned] + return b"123" + + +class SecondBadStr(object): + """ __str__ returns int """ + + def __str__(self): # [invalid-str-returned] + return 1 + + +class ThirdBadStr(object): + """ __str__ returns node which does not have 'value' in AST """ + + def __str__(self): # [invalid-str-returned] + return lambda: "some str" + + +class AmbiguousStr(object): + """ Uninferable return value """ + + __str__ = lambda self: Missing + + +class AnotherAmbiguousStr(object): + """Potential uninferable return value""" + + def __str__(self): + return str(Missing) diff --git a/tests/functional/i/invalid_str_returned.txt b/tests/functional/i/invalid_str_returned.txt new file mode 100644 index 00000000000..7a172fd29f8 --- /dev/null +++ b/tests/functional/i/invalid_str_returned.txt @@ -0,0 +1,3 @@ +invalid-str-returned:36:FirstBadStr.__str__:__str__ does not return str +invalid-str-returned:43:SecondBadStr.__str__:__str__ does not return str +invalid-str-returned:50:ThirdBadStr.__str__:__str__ does not return str From 04e72bc0f91c7085bfb2922bdf9afffde1286dff Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Wed, 27 Nov 2019 16:03:28 +0100 Subject: [PATCH 005/240] Add the new protocol checks to the documentation --- doc/whatsnew/2.5.rst | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/doc/whatsnew/2.5.rst b/doc/whatsnew/2.5.rst index 6586c5a01d1..f073830fb9a 100644 --- a/doc/whatsnew/2.5.rst +++ b/doc/whatsnew/2.5.rst @@ -19,18 +19,19 @@ New checkers f-string without having any interpolated values in it, which means that the f-string can be a normal string. -* Checks for invalid return types from protocol functions: - - * ``__bool__`` must return a bool - * ``__index__`` must return an integer - * ``__repr__`` must return a string - * ``__str__`` must return a string - * ``__bytes__`` must return bytes - * ``__hash__`` must return a string - * ``__length__hint__`` must return a non-negative integer - * ``__format__`` must return a string - * ``__getnewargs__`` must return a tuple - * ``__getnewargs_ex__`` must return a tuple of the form (tuple, dict) +* Multiple checks for invalid return types of protocol functions were added: + + * ``invalid-bool-returned``: ``__bool__`` did not return a bool + * ``invalid-index-returned``: ``__index__`` did not return an integer + * ``invalid-repr-returned)``: ``__repr__`` did not return a string + * ``invalid-str-returned)``: ``__str__`` did not return a string + * ``invalid-bytes-returned)``: ``__bytes__`` did not return a string + * ``invalid-hash-returned)``: ``__hash__`` did not return an integer + * ``invalid-length-hint-returned)``: ``__length_hint__`` did not return a non-negative integer + * ``invalid-format-returned)``: ``__format__`` did not return a string + * ``invalid-getnewargs-returned)``: ``__getnewargs__`` did not return a tuple + * ``invalid-getnewargs-ex-returned)``: ``__getnewargs_ex__`` did not return a tuple of the form (tuple, dict) + Other Changes ============= From 6d818e3b84b853fe06f4199eb8408fe6dfe033be Mon Sep 17 00:00:00 2001 From: Matthijs Blom <19817960+MatthijsBlom@users.noreply.github.com> Date: Mon, 2 Dec 2019 09:35:57 +0100 Subject: [PATCH 006/240] Add missing special method names (#3282) Close #3281 --- CONTRIBUTORS.txt | 2 ++ ChangeLog | 4 ++++ pylint/checkers/utils.py | 11 +++++------ 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 48925317a34..55804b4d370 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -354,3 +354,5 @@ contributors: * Pek Chhan: contributor * Craig Henriques: contributor + +* Matthijs Blom: contributor diff --git a/ChangeLog b/ChangeLog index 314d041a18e..4bbf23c0168 100644 --- a/ChangeLog +++ b/ChangeLog @@ -7,6 +7,10 @@ What's New in Pylint 2.5.0? Release date: TBA +* `__pow__`, `__imatmul__`, `__trunc__`, `__floor__`, and `__ceil__` are recognized as special method names. + + Close #3281 + * Added errors for protocol functions when invalid return types are detected. E0304 (invalid-bool-returned): __bool__ did not return a bool E0305 (invalid-index-returned): __index__ did not return an integer diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index 41b3876cbb3..4dbddcc8119 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -100,13 +100,10 @@ "__complex__", "__int__", "__float__", - "__neg__", - "__pos__", - "__abs__", - "__complex__", - "__int__", - "__float__", "__index__", + "__trunc__", + "__floor__", + "__ceil__", "__enter__", "__aenter__", "__getnewargs_ex__", @@ -182,11 +179,13 @@ "__cmp__", "__matmul__", "__rmatmul__", + "__imatmul__", "__div__", ), 2: ("__setattr__", "__get__", "__set__", "__setitem__", "__set_name__"), 3: ("__exit__", "__aexit__"), (0, 1): ("__round__",), + (1, 2): ("__pow__",), } SPECIAL_METHODS_PARAMS = { From f0fc81220e95aa08828ddbec9f80274d62ef0dba Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Wed, 4 Dec 2019 13:57:05 +0100 Subject: [PATCH 007/240] Fix a false positive caused by the newly added support for properties --- pylint/checkers/classes.py | 12 ++++++++---- tests/unittest_checker_classes.py | 3 +-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/pylint/checkers/classes.py b/pylint/checkers/classes.py index b8bcf9b1846..df5f69f4fd6 100644 --- a/pylint/checkers/classes.py +++ b/pylint/checkers/classes.py @@ -363,8 +363,7 @@ def _called_in_methods(func, klass, methods): def _is_attribute_property(name, klass): - """ Check if the given attribute *name* is a property - in the given *klass*. + """Check if the given attribute *name* is a property in the given *klass*. It will look for `property` calls or for functions with the given name, decorated by `property` or `property` @@ -389,8 +388,13 @@ def _is_attribute_property(name, klass): inferred ): return True - if inferred.pytype() == property_name: - return True + if inferred.pytype() != property_name: + continue + + cls = node_frame_class(inferred) + if cls == klass.declared_metaclass(): + continue + return True return False diff --git a/tests/unittest_checker_classes.py b/tests/unittest_checker_classes.py index da880362b20..f77a411e844 100644 --- a/tests/unittest_checker_classes.py +++ b/tests/unittest_checker_classes.py @@ -94,8 +94,7 @@ def __init__(self): #@ self.checker.visit_functiondef(node) def test_uninferable_attribute(self): - """Make sure protect-access doesn't raise - an exception Uninferable attributes""" + """Make sure protect-access doesn't raise an exception Uninferable attributes""" node = astroid.extract_node( """ From 2f1c979daf69b8374ba2a7515e4beef497a5baee Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Wed, 4 Dec 2019 14:03:32 +0100 Subject: [PATCH 008/240] Add regression test for property accessors. Close #2641 --- .../r/regression_property_no_member_2641.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 tests/functional/r/regression_property_no_member_2641.py diff --git a/tests/functional/r/regression_property_no_member_2641.py b/tests/functional/r/regression_property_no_member_2641.py new file mode 100644 index 00000000000..083f78bdc4a --- /dev/null +++ b/tests/functional/r/regression_property_no_member_2641.py @@ -0,0 +1,40 @@ +# pylint: disable=missing-docstring,unused-argument,too-few-public-methods +# https://github.com/PyCQA/pylint/issues/2641 +from abc import ABCMeta, abstractmethod + + +class Person(metaclass=ABCMeta): + @abstractmethod + def __init__(self, name, age): + self.name = name + self.age = age + + @property + def name(self): + return self.__name + + @name.setter + def name(self, value): + self.__name = value + + +class Myself(Person): + def __init__(self, name, age, tel): + super().__init__(name, age) + self.tel = tel + + @Person.name.setter + def name(self, value): + super(self.__class__, self.__class__).name.fset(self, "override") + + +class Wife(Person): + def __init__(self, name, age, tel): + super().__init__(name, age) + self.tel = tel + + +ms = Myself("Matheus Saraiva", 36, "988070350") +wi = Wife("Joice Saraiva", 34, "999923554") + +print(wi.name) From 1688894fe0f4ff258278a25f53ee67295bd4f657 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Wed, 4 Dec 2019 14:06:12 +0100 Subject: [PATCH 009/240] Add regression test for overriding slots with property. Close #2439 --- .../r/regression_property_slots_2439.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 tests/functional/r/regression_property_slots_2439.py diff --git a/tests/functional/r/regression_property_slots_2439.py b/tests/functional/r/regression_property_slots_2439.py new file mode 100644 index 00000000000..91cf86cfd96 --- /dev/null +++ b/tests/functional/r/regression_property_slots_2439.py @@ -0,0 +1,22 @@ +# pylint: disable=missing-docstring,invalid-name,too-few-public-methods +# https://github.com/PyCQA/pylint/issues/2439 +class TestClass: + __slots__ = ["_i"] + + def __init__(self): + self._i = 0 + + @property + def i(self): + return self._i + + @i.setter + def i(self, v): + self._i = v + + other = i + + +instance = TestClass() +instance.other = 42 +print(instance.i) From 4b31fc53a65aa2a97e7ca37309b689ca13c09b0b Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Wed, 4 Dec 2019 14:21:16 +0100 Subject: [PATCH 010/240] Add regression test for no-member of properties. Close #870 --- .../r/regression_property_no_member_870.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 tests/functional/r/regression_property_no_member_870.py diff --git a/tests/functional/r/regression_property_no_member_870.py b/tests/functional/r/regression_property_no_member_870.py new file mode 100644 index 00000000000..e89ae169e89 --- /dev/null +++ b/tests/functional/r/regression_property_no_member_870.py @@ -0,0 +1,15 @@ +# pylint: disable=too-few-public-methods,invalid-name,missing-docstring +# https://github.com/PyCQA/pylint/issues/870 + +class X: + def __init__(self, val=None): + self._val = val + @property + def val(self): + return self._val + @val.setter + def val(self, value): + self._val = value + +if __name__ == '__main__': + print(X([]).val.append) From 1e84b02c9eedf051db556df8e4966800a8c6d841 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Wed, 4 Dec 2019 14:24:01 +0100 Subject: [PATCH 011/240] Add regression test for no-member of properties. Close #844 --- .../r/regression_property_no_member_844.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/functional/r/regression_property_no_member_844.py diff --git a/tests/functional/r/regression_property_no_member_844.py b/tests/functional/r/regression_property_no_member_844.py new file mode 100644 index 00000000000..2c919fe2f48 --- /dev/null +++ b/tests/functional/r/regression_property_no_member_844.py @@ -0,0 +1,18 @@ +# pylint: disable=missing-docstring,too-few-public-methods,invalid-overridden-method +# https://github.com/PyCQA/pylint/issues/844 +class Parent: + def __init__(self): + self.__thing = 'foo' + + @property + def thing(self): + return self.__thing + + +class Child(Parent): + @Parent.thing.getter + def thing(self): + return super(Child, self).thing + '!' + + +print(Child().thing) From ce82018b8618072bf347c78d243678a4583cc05f Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Sun, 15 Dec 2019 11:19:22 +0200 Subject: [PATCH 012/240] Add regression test for property no-member error Close #3269 --- .../r/regression_property_no_member_3269.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 tests/functional/r/regression_property_no_member_3269.py diff --git a/tests/functional/r/regression_property_no_member_3269.py b/tests/functional/r/regression_property_no_member_3269.py new file mode 100644 index 00000000000..784dd90d40a --- /dev/null +++ b/tests/functional/r/regression_property_no_member_3269.py @@ -0,0 +1,23 @@ +"""Calling a super property""" +# pylint: disable=too-few-public-methods,invalid-name + +class A: + """A parent class""" + + @property + def test(self): + """A property""" + return "test" + + +class B: + """A child class""" + + @property + def test(self): + """Overriding implementation of prop which calls the parent""" + return A.test.fget(self) + " overriden" + + +if __name__ == "__main__": + print(B().test) From 88c8ee4cb76427ecba701105417e0bfa75817878 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Sun, 15 Dec 2019 11:59:14 +0100 Subject: [PATCH 013/240] Pin coverage to <5 to account for the private coverage format changing --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index f15de91edb9..bb69c70492d 100644 --- a/tox.ini +++ b/tox.ini @@ -33,7 +33,7 @@ commands = [testenv] deps = https://github.com/PyCQA/astroid/tarball/master#egg=astroid-master-2.0 - coverage + coverage<5.0 isort mccabe pytest @@ -70,7 +70,7 @@ setenv = passenv = * deps = - coverage + coverage<5.0 coveralls skip_install = true commands = @@ -83,7 +83,7 @@ changedir = {toxinidir} setenv = COVERAGE_FILE = {toxinidir}/.coverage deps = - coverage + coverage<5 skip_install = true commands = python {envsitepackagesdir}/coverage erase From e63e664edaa7775da07fb3cc1209e2d904b8adf1 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Mon, 16 Dec 2019 09:05:08 +0100 Subject: [PATCH 014/240] ``missing-*-docstring`` can look for ``__doc__`` assignments. Close #3301 --- ChangeLog | 4 ++++ pylint/checkers/base.py | 18 ++++++++++++++++++ tests/functional/m/missing_docstring.py | 4 ++++ 3 files changed, 26 insertions(+) diff --git a/ChangeLog b/ChangeLog index 4bbf23c0168..f68cdb22602 100644 --- a/ChangeLog +++ b/ChangeLog @@ -25,6 +25,10 @@ Release date: TBA Close #560 +* ``missing-*-docstring`` can look for ``__doc__`` assignments. + + Close #3301 + * ``safe_infer`` can infer a value as long as all the paths share the same type. Close #2503 diff --git a/pylint/checkers/base.py b/pylint/checkers/base.py index 6aa4f708738..d81686e5b11 100644 --- a/pylint/checkers/base.py +++ b/pylint/checkers/base.py @@ -2097,6 +2097,9 @@ def _check_docstring( ): """check the node has a non empty docstring""" docstring = node.doc + if docstring is None: + docstring = _infer_dunder_doc_attribute(node) + if docstring is None: if not report_missing: return @@ -2167,6 +2170,21 @@ def _is_one_arg_pos_call(call): return isinstance(call, astroid.Call) and len(call.args) == 1 and not call.keywords +def _infer_dunder_doc_attribute(node): + # Try to see if we have a `__doc__` attribute. + try: + docstring = node["__doc__"] + except KeyError: + return None + + docstring = utils.safe_infer(docstring) + if not docstring: + return None + if not isinstance(docstring, astroid.Const): + return None + return docstring.value + + class ComparisonChecker(_BasicChecker): """Checks for comparisons diff --git a/tests/functional/m/missing_docstring.py b/tests/functional/m/missing_docstring.py index 1169f7ce56b..72d6762aa29 100644 --- a/tests/functional/m/missing_docstring.py +++ b/tests/functional/m/missing_docstring.py @@ -52,3 +52,7 @@ def test(self, value): @test.deleter def test(self): pass + + +class DocumentedViaDunderDoc(object): + __doc__ = "This one" From 58b9e6421b2246597c97c75640ee6337460cb0ba Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Mon, 16 Dec 2019 09:26:45 +0100 Subject: [PATCH 015/240] ``undefined-variable`` can now find undefined loop iterables Close #498 --- ChangeLog | 4 ++++ pylint/checkers/variables.py | 10 +++++++++- tests/functional/u/undefined_variable.py | 7 +++++++ tests/functional/u/undefined_variable.txt | 1 + 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index f68cdb22602..997195510ba 100644 --- a/ChangeLog +++ b/ChangeLog @@ -29,6 +29,10 @@ Release date: TBA Close #3301 +* ``undefined-variable`` can now find undefined loop iterables + + Close #498 + * ``safe_infer`` can infer a value as long as all the paths share the same type. Close #2503 diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index 6a28870536b..4449bd960ea 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -546,7 +546,7 @@ def mark_as_consumed(self, name, new_node): del self.to_consume[name] def get_next_to_consume(self, node): - # mark the name as consumed if it's defined in this scope + # Get the definition of `node` from this scope name = node.name parent_node = node.parent found_node = self.to_consume.get(name) @@ -558,6 +558,14 @@ def get_next_to_consume(self, node): lhs = found_node[0].parent.targets[0] if lhs.name == name: # this name is defined in this very statement found_node = None + + if ( + found_node + and isinstance(parent_node, astroid.For) + and parent_node.iter == node + and parent_node.target in found_node + ): + found_node = None return found_node diff --git a/tests/functional/u/undefined_variable.py b/tests/functional/u/undefined_variable.py index 4323bfd3c70..4787beb9a21 100644 --- a/tests/functional/u/undefined_variable.py +++ b/tests/functional/u/undefined_variable.py @@ -274,3 +274,10 @@ def func_should_fail(_dt: datetime): # [used-before-assignment] def tick(counter: Counter, name: str, dictionary: OrderedDict) -> OrderedDict: counter[name] += 1 return dictionary + + +# pylint: disable=unused-argument +def not_using_loop_variable_accordingly(iterator): + for iteree in iteree: # [undefined-variable] + yield iteree +# pylint: enable=unused-argument diff --git a/tests/functional/u/undefined_variable.txt b/tests/functional/u/undefined_variable.txt index fa86ff5b6eb..d9aa6c86db7 100644 --- a/tests/functional/u/undefined_variable.txt +++ b/tests/functional/u/undefined_variable.txt @@ -25,3 +25,4 @@ undefined-variable:170::Undefined variable 'unicode_3' undefined-variable:225:LambdaClass4.:Undefined variable 'LambdaClass4' undefined-variable:233:LambdaClass5.:Undefined variable 'LambdaClass5' used-before-assignment:254:func_should_fail:Using variable 'datetime' before assignment +undefined-variable:281:not_using_loop_variable_accordingly:Undefined variable 'iteree' From eb33a48d2977b5b85aece9e0fd113ca13297823a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 6 Dec 2019 21:28:52 +0200 Subject: [PATCH 016/240] Fix uppercase style default regex for leading 3+ upper followed by lowercase --- ChangeLog | 1 + pylint/checkers/base.py | 2 +- tests/unittest_checker_base.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 997195510ba..06e8b16961f 100644 --- a/ChangeLog +++ b/ChangeLog @@ -158,6 +158,7 @@ Release date: TBA * Use new release of black 19.10b0 for formating +* Fix uppercase style to disallow 3+ uppercase followed by lowercase. What's New in Pylint 2.4.4? =========================== diff --git a/pylint/checkers/base.py b/pylint/checkers/base.py index d81686e5b11..62bde7cb6e8 100644 --- a/pylint/checkers/base.py +++ b/pylint/checkers/base.py @@ -126,7 +126,7 @@ class UpperCaseStyle(NamingStyle): MOD_NAME_RGX = re.compile("[A-Z_][A-Z0-9_]+$") CONST_NAME_RGX = re.compile("(([A-Z_][A-Z0-9_]*)|(__.*__))$") COMP_VAR_RGX = re.compile("[A-Z_][A-Z0-9_]+$") - DEFAULT_NAME_RGX = re.compile("([A-Z_][A-Z0-9_]{2,})|(__[a-z][a-zA-Z0-9_]+__)$") + DEFAULT_NAME_RGX = re.compile("([A-Z_][A-Z0-9_]{2,}|(__[a-z][a-zA-Z0-9_]+__))$") CLASS_ATTRIBUTE_RGX = re.compile("[A-Z_][A-Z0-9_]{2,}$") diff --git a/tests/unittest_checker_base.py b/tests/unittest_checker_base.py index aa87a2035d4..5d057131e7e 100644 --- a/tests/unittest_checker_base.py +++ b/tests/unittest_checker_base.py @@ -514,6 +514,7 @@ def test_upper_case(self): self._test_name_is_correct_for_all_name_types(naming_style, name) for name in self.ALL_NAMES - self.UPPER_CASE_NAMES: self._test_name_is_incorrect_for_all_name_types(naming_style, name) + self._test_name_is_incorrect_for_all_name_types(naming_style, "UPPERcase") self._test_should_always_pass(naming_style) From c4a954fdc354a8849a6bd3641bd64b8ce0608c73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 6 Dec 2019 21:32:54 +0200 Subject: [PATCH 017/240] Simplify naming style regexps Mostly remove unnecessary parenthesis. --- pylint/checkers/base.py | 24 +++++++++---------- tests/functional/n/namePresetCamelCase.txt | 4 ++-- .../n/name_good_bad_names_regex.txt | 2 +- tests/functional/n/name_preset_snake_case.txt | 4 ++-- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/pylint/checkers/base.py b/pylint/checkers/base.py index 62bde7cb6e8..2f3fd800f31 100644 --- a/pylint/checkers/base.py +++ b/pylint/checkers/base.py @@ -88,24 +88,24 @@ class SnakeCaseStyle(NamingStyle): """Regex rules for snake_case naming style.""" CLASS_NAME_RGX = re.compile("[a-z_][a-z0-9_]+$") - MOD_NAME_RGX = re.compile("([a-z_][a-z0-9_]*)$") - CONST_NAME_RGX = re.compile("(([a-z_][a-z0-9_]*)|(__.*__))$") + MOD_NAME_RGX = re.compile("[a-z_][a-z0-9_]*$") + CONST_NAME_RGX = re.compile("([a-z_][a-z0-9_]*|__.*__)$") COMP_VAR_RGX = re.compile("[a-z_][a-z0-9_]*$") DEFAULT_NAME_RGX = re.compile( - "(([a-z_][a-z0-9_]{2,})|(_[a-z0-9_]*)|(__[a-z][a-z0-9_]+__))$" + "([a-z_][a-z0-9_]{2,}|_[a-z0-9_]*|__[a-z][a-z0-9_]+__)$" ) - CLASS_ATTRIBUTE_RGX = re.compile(r"(([a-z_][a-z0-9_]{2,}|(__.*__)))$") + CLASS_ATTRIBUTE_RGX = re.compile(r"([a-z_][a-z0-9_]{2,}|__.*__)$") class CamelCaseStyle(NamingStyle): """Regex rules for camelCase naming style.""" CLASS_NAME_RGX = re.compile("[a-z_][a-zA-Z0-9]+$") - MOD_NAME_RGX = re.compile("([a-z_][a-zA-Z0-9]*)$") - CONST_NAME_RGX = re.compile("(([a-z_][A-Za-z0-9]*)|(__.*__))$") + MOD_NAME_RGX = re.compile("[a-z_][a-zA-Z0-9]*$") + CONST_NAME_RGX = re.compile("([a-z_][A-Za-z0-9]*|__.*__)$") COMP_VAR_RGX = re.compile("[a-z_][A-Za-z0-9]*$") - DEFAULT_NAME_RGX = re.compile("(([a-z_][a-zA-Z0-9]{2,})|(__[a-z][a-zA-Z0-9_]+__))$") - CLASS_ATTRIBUTE_RGX = re.compile(r"([a-z_][A-Za-z0-9]{2,}|(__.*__))$") + DEFAULT_NAME_RGX = re.compile("([a-z_][a-zA-Z0-9]{2,}|__[a-z][a-zA-Z0-9_]+__)$") + CLASS_ATTRIBUTE_RGX = re.compile(r"([a-z_][A-Za-z0-9]{2,}|__.*__)$") class PascalCaseStyle(NamingStyle): @@ -113,9 +113,9 @@ class PascalCaseStyle(NamingStyle): CLASS_NAME_RGX = re.compile("[A-Z_][a-zA-Z0-9]+$") MOD_NAME_RGX = re.compile("[A-Z_][a-zA-Z0-9]+$") - CONST_NAME_RGX = re.compile("(([A-Z_][A-Za-z0-9]*)|(__.*__))$") + CONST_NAME_RGX = re.compile("([A-Z_][A-Za-z0-9]*|__.*__)$") COMP_VAR_RGX = re.compile("[A-Z_][a-zA-Z0-9]+$") - DEFAULT_NAME_RGX = re.compile("[A-Z_][a-zA-Z0-9]{2,}$|(__[a-z][a-zA-Z0-9_]+__)$") + DEFAULT_NAME_RGX = re.compile("([A-Z_][a-zA-Z0-9]{2,}|__[a-z][a-zA-Z0-9_]+__)$") CLASS_ATTRIBUTE_RGX = re.compile("[A-Z_][a-zA-Z0-9]{2,}$") @@ -124,9 +124,9 @@ class UpperCaseStyle(NamingStyle): CLASS_NAME_RGX = re.compile("[A-Z_][A-Z0-9_]+$") MOD_NAME_RGX = re.compile("[A-Z_][A-Z0-9_]+$") - CONST_NAME_RGX = re.compile("(([A-Z_][A-Z0-9_]*)|(__.*__))$") + CONST_NAME_RGX = re.compile("([A-Z_][A-Z0-9_]*|__.*__)$") COMP_VAR_RGX = re.compile("[A-Z_][A-Z0-9_]+$") - DEFAULT_NAME_RGX = re.compile("([A-Z_][A-Z0-9_]{2,}|(__[a-z][a-zA-Z0-9_]+__))$") + DEFAULT_NAME_RGX = re.compile("([A-Z_][A-Z0-9_]{2,}|__[a-z][a-zA-Z0-9_]+__)$") CLASS_ATTRIBUTE_RGX = re.compile("[A-Z_][A-Z0-9_]{2,}$") diff --git a/tests/functional/n/namePresetCamelCase.txt b/tests/functional/n/namePresetCamelCase.txt index 3c31bbcb0c2..68bb5aa4bd2 100644 --- a/tests/functional/n/namePresetCamelCase.txt +++ b/tests/functional/n/namePresetCamelCase.txt @@ -1,3 +1,3 @@ -invalid-name:3::"Constant name ""SOME_CONSTANT"" doesn't conform to camelCase naming style ('(([a-z_][A-Za-z0-9]*)|(__.*__))$' pattern)" +invalid-name:3::"Constant name ""SOME_CONSTANT"" doesn't conform to camelCase naming style ('([a-z_][A-Za-z0-9]*|__.*__)$' pattern)" invalid-name:10:MyClass:"Class name ""MyClass"" doesn't conform to camelCase naming style ('[a-z_][a-zA-Z0-9]+$' pattern)" -invalid-name:22:say_hello:"Function name ""say_hello"" doesn't conform to camelCase naming style ('(([a-z_][a-zA-Z0-9]{2,})|(__[a-z][a-zA-Z0-9_]+__))$' pattern)" +invalid-name:22:say_hello:"Function name ""say_hello"" doesn't conform to camelCase naming style ('([a-z_][a-zA-Z0-9]{2,}|__[a-z][a-zA-Z0-9_]+__)$' pattern)" diff --git a/tests/functional/n/name_good_bad_names_regex.txt b/tests/functional/n/name_good_bad_names_regex.txt index 0ec33bd4d0a..5cdef2f4db6 100644 --- a/tests/functional/n/name_good_bad_names_regex.txt +++ b/tests/functional/n/name_good_bad_names_regex.txt @@ -1,3 +1,3 @@ blacklisted-name:5::"Black listed name ""explicit_bad_some_constant""" -invalid-name:7::"Constant name ""snake_case_bad_SOME_CONSTANT"" doesn't conform to snake_case naming style ('(([a-z_][a-z0-9_]*)|(__.*__))$' pattern)" +invalid-name:7::"Constant name ""snake_case_bad_SOME_CONSTANT"" doesn't conform to snake_case naming style ('([a-z_][a-z0-9_]*|__.*__)$' pattern)" blacklisted-name:19:blacklisted_2_snake_case:"Black listed name ""blacklisted_2_snake_case""" diff --git a/tests/functional/n/name_preset_snake_case.txt b/tests/functional/n/name_preset_snake_case.txt index 14a3d653023..c4c043961dc 100644 --- a/tests/functional/n/name_preset_snake_case.txt +++ b/tests/functional/n/name_preset_snake_case.txt @@ -1,3 +1,3 @@ -invalid-name:3::"Constant name ""SOME_CONSTANT"" doesn't conform to snake_case naming style ('(([a-z_][a-z0-9_]*)|(__.*__))$' pattern)" +invalid-name:3::"Constant name ""SOME_CONSTANT"" doesn't conform to snake_case naming style ('([a-z_][a-z0-9_]*|__.*__)$' pattern)" invalid-name:10:MyClass:"Class name ""MyClass"" doesn't conform to snake_case naming style ('[a-z_][a-z0-9_]+$' pattern)" -invalid-name:22:sayHello:"Function name ""sayHello"" doesn't conform to snake_case naming style ('(([a-z_][a-z0-9_]{2,})|(_[a-z0-9_]*)|(__[a-z][a-z0-9_]+__))$' pattern)" +invalid-name:22:sayHello:"Function name ""sayHello"" doesn't conform to snake_case naming style ('([a-z_][a-z0-9_]{2,}|_[a-z0-9_]*|__[a-z][a-z0-9_]+__)$' pattern)" From dc83a86bc5556e2ec630403f760461708f15a69b Mon Sep 17 00:00:00 2001 From: Andy Palmer <25123779+ninezerozeronine@users.noreply.github.com> Date: Mon, 16 Dec 2019 00:38:21 -0800 Subject: [PATCH 018/240] Add check to make sure only strings are assigned to __name__ (#3271) Close #583 --- CONTRIBUTORS.txt | 2 + ChangeLog | 4 ++ doc/whatsnew/2.5.rst | 2 + pylint/checkers/typecheck.py | 45 +++++++++++++++- .../n/non_str_assignment_to_dunder_name.py | 53 +++++++++++++++++++ .../n/non_str_assignment_to_dunder_name.txt | 8 +++ 6 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 tests/functional/n/non_str_assignment_to_dunder_name.py create mode 100644 tests/functional/n/non_str_assignment_to_dunder_name.txt diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 55804b4d370..d1ca0b4c52b 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -356,3 +356,5 @@ contributors: * Craig Henriques: contributor * Matthijs Blom: contributor + +* Andy Palmer: contributor diff --git a/ChangeLog b/ChangeLog index 06e8b16961f..4aa396cf75c 100644 --- a/ChangeLog +++ b/ChangeLog @@ -7,6 +7,10 @@ What's New in Pylint 2.5.0? Release date: TBA +* Add a check for non string assignment to __name__ attribute. + + Close #583 + * `__pow__`, `__imatmul__`, `__trunc__`, `__floor__`, and `__ceil__` are recognized as special method names. Close #3281 diff --git a/doc/whatsnew/2.5.rst b/doc/whatsnew/2.5.rst index f073830fb9a..d5adca647a0 100644 --- a/doc/whatsnew/2.5.rst +++ b/doc/whatsnew/2.5.rst @@ -66,3 +66,5 @@ separated list of regexes, that if a name matches will be always marked as a bla * Mutable ``collections.*`` are now flagged as dangerous defaults. * Add new --fail-under flag for setting the threshold for the score to fail overall tests. If the score is over the fail-under threshold, pylint will complete SystemExit with value 0 to indicate no errors. + +* Add a new check (non-str-assignment-to-dunder-name) to ensure that only strings are assigned to ``__name__`` attributes \ No newline at end of file diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index 3371926e5e8..d6685e48213 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -27,6 +27,7 @@ # Copyright (c) 2018 Konstantin # Copyright (c) 2018 Justin Li # Copyright (c) 2018 Bryce Guinta +# Copyright (c) 2019 Andy Palmer # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html # For details: https://github.com/PyCQA/pylint/blob/master/COPYING @@ -389,6 +390,11 @@ def _missing_member_hint(owner, attrname, distance_threshold, max_choices): "Emitted when the caller's argument names fully match the parameter " "names in the function signature but do not have the same order.", ), + "W1115": ( + "Non-string value assigned to __name__", + "non-str-assignment-to-dunder-name", + "Emitted when a non-string vaue is assigned to __name__", + ), } # builtin sequence types in Python 2 and 3. @@ -986,8 +992,20 @@ def _get_nomember_msgid_hint(self, node, owner): hint = "" return msg, hint - @check_messages("assignment-from-no-return", "assignment-from-none") + @check_messages( + "assignment-from-no-return", + "assignment-from-none", + "non-str-assignment-to-dunder-name", + ) def visit_assign(self, node): + """ + Process assignments in the AST. + """ + + self._check_assignment_from_function_call(node) + self._check_dundername_is_string(node) + + def _check_assignment_from_function_call(self, node): """check that if assigning to a function call, the function is possibly returning something valuable """ @@ -1035,6 +1053,31 @@ def visit_assign(self, node): else: self.add_message("assignment-from-none", node=node) + def _check_dundername_is_string(self, node): + """ + Check a string is assigned to self.__name__ + """ + + # Check the left hand side of the assignment is .__name__ + lhs = node.targets[0] + if not isinstance(lhs, astroid.node_classes.AssignAttr): + return + if not lhs.attrname == "__name__": + return + + # If the right hand side is not a string + rhs = node.value + if isinstance(rhs, astroid.Const) and isinstance(rhs.value, str): + return + inferred = utils.safe_infer(rhs) + if not inferred: + return + if not ( + isinstance(inferred, astroid.Const) and isinstance(inferred.value, str) + ): + # Add the message + self.add_message("non-str-assignment-to-dunder-name", node=node) + def _check_uninferable_call(self, node): """ Check that the given uninferable Call node does not diff --git a/tests/functional/n/non_str_assignment_to_dunder_name.py b/tests/functional/n/non_str_assignment_to_dunder_name.py new file mode 100644 index 00000000000..a910cbd6f2a --- /dev/null +++ b/tests/functional/n/non_str_assignment_to_dunder_name.py @@ -0,0 +1,53 @@ +# pylint: disable=missing-module-docstring, missing-class-docstring +# pylint: disable=too-few-public-methods, missing-function-docstring +# pylint: disable=import-error + +import random + +from unknown import Unknown + + +class ExampleClass(): + pass + + +def example_function(): + pass + + +def returns_str(): + return "abcd" + + +def returns_int(): + return 0 + + +def returns_tuple(): + return 0, "abc" + + +# Might not be thorough if same hash seed is used in testing... +def returns_random_type(): + if random.randint(0, 1) > 0: + return 0 + + return "abc" + +ExampleClass.__name__ = 1 # [non-str-assignment-to-dunder-name] +ExampleClass.__name__ = True # [non-str-assignment-to-dunder-name] +ExampleClass.__name__ = returns_tuple() # [non-str-assignment-to-dunder-name] +ExampleClass.__name__ = returns_int() # [non-str-assignment-to-dunder-name] +ExampleClass.__name__ = "foo" +ExampleClass.__name__ = returns_str() +ExampleClass.__name__ = returns_random_type() +ExampleClass.__name__ = Unknown + +example_function.__name__ = 1 # [non-str-assignment-to-dunder-name] +example_function.__name__ = True # [non-str-assignment-to-dunder-name] +example_function.__name__ = returns_tuple() # [non-str-assignment-to-dunder-name] +example_function.__name__ = returns_int() # [non-str-assignment-to-dunder-name] +example_function.__name__ = "foo" +example_function.__name__ = returns_str() +example_function.__name__ = returns_random_type() +example_function.__name__ = Unknown diff --git a/tests/functional/n/non_str_assignment_to_dunder_name.txt b/tests/functional/n/non_str_assignment_to_dunder_name.txt new file mode 100644 index 00000000000..23af6681c7c --- /dev/null +++ b/tests/functional/n/non_str_assignment_to_dunder_name.txt @@ -0,0 +1,8 @@ +non-str-assignment-to-dunder-name:37::Non-string value assigned to __name__ +non-str-assignment-to-dunder-name:38::Non-string value assigned to __name__ +non-str-assignment-to-dunder-name:39::Non-string value assigned to __name__ +non-str-assignment-to-dunder-name:40::Non-string value assigned to __name__ +non-str-assignment-to-dunder-name:46::Non-string value assigned to __name__ +non-str-assignment-to-dunder-name:47::Non-string value assigned to __name__ +non-str-assignment-to-dunder-name:48::Non-string value assigned to __name__ +non-str-assignment-to-dunder-name:49::Non-string value assigned to __name__ From d128c276435c379325ad89d0680f889e16364d1f Mon Sep 17 00:00:00 2001 From: Nick Drozd Date: Tue, 24 Dec 2019 03:10:52 -0600 Subject: [PATCH 019/240] Enable else-if-used extension (#3316) After all, if we don't use this, why should anybody else? --- pylint/checkers/base.py | 13 ++++++------- pylint/checkers/classes.py | 2 +- pylint/checkers/format.py | 22 ++++++++++------------ pylint/checkers/python3.py | 9 ++++----- pylint/checkers/typecheck.py | 24 +++++++++++------------- pylint/checkers/variables.py | 26 ++++++++++++-------------- pylint/lint.py | 12 +++++------- pylintrc | 1 + 8 files changed, 50 insertions(+), 59 deletions(-) diff --git a/pylint/checkers/base.py b/pylint/checkers/base.py index 2f3fd800f31..3c7b90249ba 100644 --- a/pylint/checkers/base.py +++ b/pylint/checkers/base.py @@ -1885,13 +1885,12 @@ def visit_assignname(self, node): if isinstance(assign_type, astroid.Assign) and not in_loop(assign_type): if isinstance(utils.safe_infer(assign_type.value), astroid.ClassDef): self._check_name("class", node.name, node) - else: - # Don't emit if the name redefines an import - # in an ImportError except handler. - if not _redefines_import(node) and isinstance( - utils.safe_infer(assign_type.value), astroid.Const - ): - self._check_name("const", node.name, node) + # Don't emit if the name redefines an import + # in an ImportError except handler. + elif not _redefines_import(node) and isinstance( + utils.safe_infer(assign_type.value), astroid.Const + ): + self._check_name("const", node.name, node) elif isinstance(assign_type, astroid.ExceptHandler): self._check_name("variable", node.name, node) elif isinstance(frame, astroid.FunctionDef): diff --git a/pylint/checkers/classes.py b/pylint/checkers/classes.py index df5f69f4fd6..d43253467d0 100644 --- a/pylint/checkers/classes.py +++ b/pylint/checkers/classes.py @@ -1523,7 +1523,7 @@ def _check_first_arg_for_type(self, node, metaclass=0): node.name, ) # regular class - else: + else: # pylint: disable=else-if-used # class method if node.type == "classmethod" or node.name == "__class_getitem__": self._check_first_arg_config( diff --git a/pylint/checkers/format.py b/pylint/checkers/format.py index 0463bcc91c4..b40e8c469a0 100644 --- a/pylint/checkers/format.py +++ b/pylint/checkers/format.py @@ -1176,18 +1176,16 @@ def visit_default(self, node): prev_sibl = node.previous_sibling() if prev_sibl is not None: prev_line = prev_sibl.fromlineno + # The line on which a finally: occurs in a try/finally + # is not directly represented in the AST. We infer it + # by taking the last line of the body and adding 1, which + # should be the line of finally: + elif ( + isinstance(node.parent, nodes.TryFinally) and node in node.parent.finalbody + ): + prev_line = node.parent.body[0].tolineno + 1 else: - # The line on which a finally: occurs in a try/finally - # is not directly represented in the AST. We infer it - # by taking the last line of the body and adding 1, which - # should be the line of finally: - if ( - isinstance(node.parent, nodes.TryFinally) - and node in node.parent.finalbody - ): - prev_line = node.parent.body[0].tolineno + 1 - else: - prev_line = node.parent.statement().fromlineno + prev_line = node.parent.statement().fromlineno line = node.fromlineno assert line, node if prev_line == line and self._visited_lines.get(line) != 2: @@ -1275,7 +1273,7 @@ def check_line_length(self, line: str, i: int) -> None: @staticmethod def remove_pylint_option_from_lines(options_pattern_obj) -> str: """ - Remove the `# pylint ...` pattern from lines + Remove the `# pylint ...` pattern from lines """ lines = options_pattern_obj.string purged_lines = ( diff --git a/pylint/checkers/python3.py b/pylint/checkers/python3.py index d4b6ab59ad8..9e248bea93d 100644 --- a/pylint/checkers/python3.py +++ b/pylint/checkers/python3.py @@ -1212,11 +1212,10 @@ def visit_call(self, node): return if node.func.attrname == "next": self.add_message("next-method-called", node=node) - else: - if node.func.attrname in ("iterkeys", "itervalues", "iteritems"): - self.add_message("dict-iter-method", node=node) - elif node.func.attrname in ("viewkeys", "viewvalues", "viewitems"): - self.add_message("dict-view-method", node=node) + elif node.func.attrname in ("iterkeys", "itervalues", "iteritems"): + self.add_message("dict-iter-method", node=node) + elif node.func.attrname in ("viewkeys", "viewvalues", "viewitems"): + self.add_message("dict-view-method", node=node) elif isinstance(node.func, astroid.Name): found_node = node.func.lookup(node.func.name)[0] if _is_builtin(found_node): diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index d6685e48213..92b14a992e5 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -547,12 +547,11 @@ def _no_context_variadic_keywords(node, scope): if isinstance(scope, astroid.Lambda) and not isinstance(scope, astroid.FunctionDef): variadics = list(node.keywords or []) + node.kwargs - else: - if isinstance(statement, (astroid.Return, astroid.Expr)) and isinstance( - statement.value, astroid.Call - ): - call = statement.value - variadics = list(call.keywords or []) + call.kwargs + elif isinstance(statement, (astroid.Return, astroid.Expr)) and isinstance( + statement.value, astroid.Call + ): + call = statement.value + variadics = list(call.keywords or []) + call.kwargs return _no_context_variadic(node, scope.args.kwarg, astroid.Keyword, variadics) @@ -1296,13 +1295,12 @@ def visit_call(self, node): # The remaining positional arguments get assigned to the *args # parameter. break - else: - if not overload_function: - # Too many positional arguments. - self.add_message( - "too-many-function-args", node=node, args=(callable_name,) - ) - break + elif not overload_function: + # Too many positional arguments. + self.add_message( + "too-many-function-args", node=node, args=(callable_name,) + ) + break # 2. Match the keyword arguments. for keyword in keyword_args: diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index 4449bd960ea..d4354478160 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -1290,13 +1290,12 @@ def _is_variable_violation( use_outer_definition = stmt == defstmt and not isinstance( defnode, astroid.node_classes.Comprehension ) - else: - # check if we have a nonlocal - if name in defframe.locals: - maybee0601 = not any( - isinstance(child, astroid.Nonlocal) and name in child.names - for child in defframe.get_children() - ) + # check if we have a nonlocal + elif name in defframe.locals: + maybee0601 = not any( + isinstance(child, astroid.Nonlocal) and name in child.names + for child in defframe.get_children() + ) if ( base_scope_type == "lambda" @@ -1780,13 +1779,12 @@ def _check_unpacking(self, inferred, node, targets): ), ) # attempt to check unpacking may be possible (ie RHS is iterable) - else: - if not utils.is_iterable(inferred): - self.add_message( - "unpacking-non-sequence", - node=node, - args=(_get_unpacking_extra_info(node, inferred),), - ) + elif not utils.is_iterable(inferred): + self.add_message( + "unpacking-non-sequence", + node=node, + args=(_get_unpacking_extra_info(node, inferred),), + ) def _check_module_attrs(self, node, module, module_names): """check that module_names (list of string) are accessible through the diff --git a/pylint/lint.py b/pylint/lint.py index c85d512ecc6..c69e62dd925 100644 --- a/pylint/lint.py +++ b/pylint/lint.py @@ -138,11 +138,10 @@ def _merge_stats(stats): for key, item in stat.items(): if key not in merged: merged[key] = item + elif isinstance(item, dict): + merged[key].update(item) else: - if isinstance(item, dict): - merged[key].update(item) - else: - merged[key] = merged[key] + item + merged[key] = merged[key] + item merged["by_msg"] = by_msg return merged @@ -1757,9 +1756,8 @@ def __init__(self, args, reporter=None, do_exit=True): file=sys.stderr, ) linter.set_option("jobs", 1) - else: - if linter.config.jobs == 0: - linter.config.jobs = _cpu_count() + elif linter.config.jobs == 0: + linter.config.jobs = _cpu_count() # We have loaded configuration from config file and command line. Now, we can # load plugin specific configuration. diff --git a/pylintrc b/pylintrc index dbd52655266..c75c3fa02dc 100644 --- a/pylintrc +++ b/pylintrc @@ -17,6 +17,7 @@ persistent=yes # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. load-plugins= + pylint.extensions.check_elif # Use multiple processes to speed up Pylint. jobs=1 From bcc57067246488364e6d72453b801f5bb9bf1505 Mon Sep 17 00:00:00 2001 From: Ashley Whetter Date: Fri, 3 Jan 2020 16:03:31 -0800 Subject: [PATCH 020/240] Fixed undefined-variable and unused-import flase positives when using a metaclass via an attribute Closes #1603 --- ChangeLog | 5 +++++ pylint/checkers/variables.py | 6 ++++-- tests/functional/u/undefined_variable_py30.py | 6 +++++- tests/functional/u/undefined_variable_py30.txt | 4 ++-- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/ChangeLog b/ChangeLog index 4aa396cf75c..a722ae50b92 100644 --- a/ChangeLog +++ b/ChangeLog @@ -164,6 +164,11 @@ Release date: TBA * Fix uppercase style to disallow 3+ uppercase followed by lowercase. +* Fixed ``undefined-variable`` and ``unused-import`` false positives + when using a metaclass via an attribute. + + Close #1603 + What's New in Pylint 2.4.4? =========================== Release date: 2019-11-13 diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index d4354478160..205b89d756d 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -1966,6 +1966,8 @@ def _check_classdef_metaclasses(self, klass, parent_node): name = None if isinstance(klass._metaclass, astroid.Name): name = klass._metaclass.name + elif isinstance(klass._metaclass, astroid.Attribute) and klass._metaclass.expr: + name = klass._metaclass.expr.name elif metaclass: name = metaclass.root().name @@ -1983,8 +1985,8 @@ def _check_classdef_metaclasses(self, klass, parent_node): name = None if isinstance(klass._metaclass, astroid.Name): name = klass._metaclass.name - elif isinstance(klass._metaclass, astroid.Attribute): - name = klass._metaclass.as_string() + elif isinstance(klass._metaclass, astroid.Attribute) and klass._metaclass.expr: + name = klass._metaclass.expr.name if name is not None: if not ( diff --git a/tests/functional/u/undefined_variable_py30.py b/tests/functional/u/undefined_variable_py30.py index 3ccdfa63511..78d6dd3ea0f 100644 --- a/tests/functional/u/undefined_variable_py30.py +++ b/tests/functional/u/undefined_variable_py30.py @@ -1,6 +1,6 @@ """Test warnings about access to undefined variables for various Python 3 constructs. """ -# pylint: disable=too-few-public-methods, no-init, no-self-use +# pylint: disable=too-few-public-methods, no-init, no-self-use, import-error # pylint: disable=wrong-import-position, invalid-metaclass, useless-object-inheritance class Undefined: """ test various annotation problems. """ @@ -57,6 +57,7 @@ def test_bad1(self, *args: trop1): # [undefined-variable] def test_bad2(self, **bac: trop2): # [undefined-variable] """ trop2 is undefined at this moment. """ +import abc from abc import ABCMeta class Bad(metaclass=ABCMet): # [undefined-variable] @@ -77,6 +78,9 @@ class ThirdGood(metaclass=ABCMeta): class FourthGood(ThirdGood): """ This should not trigger anything. """ +class FifthGood(metaclass=abc.Metaclass): + """Metaclasses can come from imported modules.""" + # The following used to raise used-before-assignment # pylint: disable=missing-docstring, multiple-statements def used_before_assignment(*, arg): return arg + 1 diff --git a/tests/functional/u/undefined_variable_py30.txt b/tests/functional/u/undefined_variable_py30.txt index 28f67d3540b..36b653f853c 100644 --- a/tests/functional/u/undefined_variable_py30.txt +++ b/tests/functional/u/undefined_variable_py30.txt @@ -4,5 +4,5 @@ undefined-variable:36:Undefined1.InnerScope.test1:Undefined variable 'ABC' undefined-variable:51:FalsePositive342.test_bad:Undefined variable 'trop' undefined-variable:54:FalsePositive342.test_bad1:Undefined variable 'trop1' undefined-variable:57:FalsePositive342.test_bad2:Undefined variable 'trop2' -undefined-variable:62:Bad:Undefined variable 'ABCMet' -undefined-variable:65:SecondBad:Undefined variable 'ab.ABCMeta' \ No newline at end of file +undefined-variable:63:Bad:Undefined variable 'ABCMet' +undefined-variable:66:SecondBad:Undefined variable 'ab' From 21f25ccafe87195318f2eff20b65e5be33a0ac6d Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Sat, 4 Jan 2020 09:36:02 +0100 Subject: [PATCH 021/240] Fix formatting error --- pylint/checkers/variables.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index 205b89d756d..bf723a2fa0c 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -1985,7 +1985,10 @@ def _check_classdef_metaclasses(self, klass, parent_node): name = None if isinstance(klass._metaclass, astroid.Name): name = klass._metaclass.name - elif isinstance(klass._metaclass, astroid.Attribute) and klass._metaclass.expr: + elif ( + isinstance(klass._metaclass, astroid.Attribute) + and klass._metaclass.expr + ): name = klass._metaclass.expr.name if name is not None: From bd6aa15de9f9b9ad848ccbc49fef06011cb95e1e Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Sun, 5 Jan 2020 11:35:43 +0100 Subject: [PATCH 022/240] Mark --rcfile as a command since it cannot used in the configuration file. Found in #3329 --- pylint/lint.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pylint/lint.py b/pylint/lint.py index c69e62dd925..578e847ff2b 100644 --- a/pylint/lint.py +++ b/pylint/lint.py @@ -1531,9 +1531,10 @@ def __init__(self, args, reporter=None, do_exit=True): { "action": "callback", "callback": Run._return_one, + "group": "Commands", "type": "string", "metavar": "", - "help": "Specify a configuration file.", + "help": "Specify a configuration file to load.", }, ), ( From 06d52873188df2e27934da6bac3b67cea008b9a9 Mon Sep 17 00:00:00 2001 From: Wes Turner Date: Wed, 18 Dec 2019 13:14:56 +0100 Subject: [PATCH 023/240] Adds a new check 'inconsistent-quotes'. Quoting PEP-8: In Python, single-quoted strings and double-quoted strings are the same. This PEP does not make a recommendation for this. Pick a rule and stick to it. When a string contains single or double quote characters, however, use the other one to avoid backslashes in the string. It improves readability. For triple-quoted strings, always use double quote characters to be consistent with the docstring convention in PEP 257. Features: Accounts for strings where the delimiter is swapped so an internal quote doesn't need to be escaped Only errors on those lines that represent the module's minority delimiter. Ignores longstrings (they could be docstrings, and checking those delimiters is someone else's responsibility) --- CONTRIBUTORS.txt | 2 + ChangeLog | 2 + doc/whatsnew/2.5.rst | 10 +- examples/pylintrc | 4 + pylint/checkers/strings.py | 157 +++++++++++++++++++- tests/functional/i/inconsistent_quotes.py | 16 ++ tests/functional/i/inconsistent_quotes.rc | 2 + tests/functional/i/inconsistent_quotes.txt | 1 + tests/functional/i/inconsistent_quotes2.py | 10 ++ tests/functional/i/inconsistent_quotes2.rc | 2 + tests/functional/i/inconsistent_quotes2.txt | 1 + 11 files changed, 204 insertions(+), 3 deletions(-) create mode 100644 tests/functional/i/inconsistent_quotes.py create mode 100644 tests/functional/i/inconsistent_quotes.rc create mode 100644 tests/functional/i/inconsistent_quotes.txt create mode 100644 tests/functional/i/inconsistent_quotes2.py create mode 100644 tests/functional/i/inconsistent_quotes2.rc create mode 100644 tests/functional/i/inconsistent_quotes2.txt diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index d1ca0b4c52b..edd19e6fd8f 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -358,3 +358,5 @@ contributors: * Matthijs Blom: contributor * Andy Palmer: contributor + +* Wes Turner (Google): added new check 'inconsistent-quotes' diff --git a/ChangeLog b/ChangeLog index a722ae50b92..fc531eb993b 100644 --- a/ChangeLog +++ b/ChangeLog @@ -7,6 +7,8 @@ What's New in Pylint 2.5.0? Release date: TBA +* A new check `inconsistent-quotes` was added. + * Add a check for non string assignment to __name__ attribute. Close #583 diff --git a/doc/whatsnew/2.5.rst b/doc/whatsnew/2.5.rst index d5adca647a0..f37ff257918 100644 --- a/doc/whatsnew/2.5.rst +++ b/doc/whatsnew/2.5.rst @@ -32,6 +32,14 @@ New checkers * ``invalid-getnewargs-returned)``: ``__getnewargs__`` did not return a tuple * ``invalid-getnewargs-ex-returned)``: ``__getnewargs_ex__`` did not return a tuple of the form (tuple, dict) +* A new check ``inconsistent-quotes`` was added. + + This check is emitted when quotes delimiters (" and ') are not used + consistently throughout a module. It makes allowances for avoiding + unnecessary escaping, allowing, for example, ``"Don't error"`` in a module in + which single-quotes otherwise delimit strings so that the single quote in + ``Don't`` doesn't need to be escaped. + Other Changes ============= @@ -67,4 +75,4 @@ separated list of regexes, that if a name matches will be always marked as a bla * Add new --fail-under flag for setting the threshold for the score to fail overall tests. If the score is over the fail-under threshold, pylint will complete SystemExit with value 0 to indicate no errors. -* Add a new check (non-str-assignment-to-dunder-name) to ensure that only strings are assigned to ``__name__`` attributes \ No newline at end of file +* Add a new check (non-str-assignment-to-dunder-name) to ensure that only strings are assigned to ``__name__`` attributes diff --git a/examples/pylintrc b/examples/pylintrc index 1277bd21b2a..fd1befdaf57 100644 --- a/examples/pylintrc +++ b/examples/pylintrc @@ -476,6 +476,10 @@ notes=FIXME, # several lines. check-str-concat-over-line-jumps=no +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + [IMPORTS] diff --git a/pylint/checkers/strings.py b/pylint/checkers/strings.py index c5594b61c93..85b89fb490f 100644 --- a/pylint/checkers/strings.py +++ b/pylint/checkers/strings.py @@ -23,9 +23,11 @@ """ import builtins +import collections import numbers +import re import tokenize -from collections import Counter +from typing import Counter, Iterable import astroid from astroid.arguments import CallSite @@ -36,6 +38,34 @@ from pylint.interfaces import IAstroidChecker, IRawChecker, ITokenChecker _AST_NODE_STR_TYPES = ("__builtin__.unicode", "__builtin__.str", "builtins.str") +# Prefixes for both strings and bytes literals per +# https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals +_PREFIXES = { + "r", + "u", + "R", + "U", + "f", + "F", + "fr", + "Fr", + "fR", + "FR", + "rf", + "rF", + "Rf", + "RF", + "b", + "B", + "br", + "Br", + "bR", + "BR", + "rb", + "rB", + "Rb", + "RB", +} MSGS = { "E1300": ( @@ -385,7 +415,7 @@ def visit_call(self, node): self._check_new_format(node, func) def _detect_vacuous_formatting(self, node, positional_arguments): - counter = Counter( + counter = collections.Counter( arg.name for arg in positional_arguments if isinstance(arg, astroid.Name) ) for name, count in counter.items(): @@ -600,6 +630,12 @@ class StringConstantChecker(BaseTokenChecker): "maybe a comma is missing ?", {"old_names": [("W1403", "implicit-str-concat-in-sequence")]}, ), + "W1405": ( + "Quote delimiter %s is inconsistent with the rest of the file", + "inconsistent-quotes", + "Quote delimiters are not used consistently throughout a module " + "(with allowances made for avoiding unnecessary escaping).", + ), } options = ( ( @@ -614,6 +650,17 @@ class StringConstantChecker(BaseTokenChecker): "several lines.", }, ), + ( + "check-quote-consistency", + { + "default": False, + "type": "yn", + "metavar": "", + "help": "This flag controls whether inconsistent-quotes generates a " + "warning when the character used as a quote delimiter is used " + "inconsistently within a module.", + }, + ), ) # Characters that have a special meaning after a backslash in either @@ -656,6 +703,9 @@ def process_tokens(self, tokens): start = (start[0], len(line[: start[1]].encode(encoding))) self.string_tokens[start] = (str_eval(token), next_token) + if self.config.check_quote_consistency: + self.check_for_consistent_string_delimiters(tokens) + @check_messages("implicit-str-concat") def visit_list(self, node): self.check_for_concatenated_strings(node.elts, "list") @@ -672,6 +722,40 @@ def visit_assign(self, node): if isinstance(node.value, astroid.Const) and isinstance(node.value.value, str): self.check_for_concatenated_strings([node.value], "assignment") + def check_for_consistent_string_delimiters( + self, tokens: Iterable[tokenize.TokenInfo] + ) -> None: + """Adds a message for each string using inconsistent quote delimiters. + + Quote delimiters are used inconsistently if " and ' are mixed in a module's + shortstrings without having done so to avoid escaping an internal quote + character. + + Args: + tokens: The tokens to be checked against for consistent usage. + """ + string_delimiters: Counter[str] = collections.Counter() + + # First, figure out which quote character predominates in the module + for tok_type, token, _, _, _ in tokens: + if tok_type == tokenize.STRING and _is_quote_delimiter_chosen_freely(token): + string_delimiters[_get_quote_delimiter(token)] += 1 + + if len(string_delimiters) > 1: + # Ties are broken arbitrarily + most_common_delimiter = string_delimiters.most_common(1)[0][0] + for tok_type, token, start, _, _ in tokens: + if tok_type != tokenize.STRING: + continue + quote_delimiter = _get_quote_delimiter(token) + if ( + _is_quote_delimiter_chosen_freely(token) + and quote_delimiter != most_common_delimiter + ): + self.add_message( + "inconsistent-quotes", line=start[0], args=(quote_delimiter,) + ) + def check_for_concatenated_strings(self, elements, iterable_type): for elt in elements: if not (isinstance(elt, Const) and elt.pytype() in _AST_NODE_STR_TYPES): @@ -787,3 +871,72 @@ def str_eval(token): if token[0:3] in ('"""', "'''"): return token[3:-3] return token[1:-1] + + +def _is_long_string(string_token: str) -> bool: + """Is this string token a "longstring" (is it triple-quoted)? + + Long strings are triple-quoted as defined in + https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals + + This function only checks characters up through the open quotes. Because it's meant + to be applied only to tokens that represent string literals, it doesn't bother to + check for close-quotes (demonstrating that the literal is a well-formed string). + + Args: + string_token: The string token to be parsed. + + Returns: + A boolean representing whether or not this token matches a longstring + regex. + """ + single_quoted_regex = "(%s)?'''" % "|".join(_PREFIXES) + double_quoted_regex = '(%s)?"""' % "|".join(_PREFIXES) + + return bool( + re.match(single_quoted_regex, string_token) + or re.match(double_quoted_regex, string_token) + ) + + +def _get_quote_delimiter(string_token: str) -> str: + """Returns the quote character used to delimit this token string. + + This function does little checking for whether the token is a well-formed + string. + + Args: + string_token: The token to be parsed. + + Returns: + A string containing solely the first quote delimiter character in the passed + string. + + Raises: + ValueError: No quote delimiter characters are present. + """ + match = re.match("(%s)?(\"|')" % "|".join(_PREFIXES), string_token, re.DOTALL) + if not match: + raise ValueError("string token %s is not a well-formed string" % string_token) + return match.group(2) + + +def _is_quote_delimiter_chosen_freely(string_token: str) -> bool: + """Was there a non-awkward option for the quote delimiter? + + Args: + string_token: The quoted string whose delimiters are to be checked. + + Returns: + Whether there was a choice in this token's quote character that would + not have involved backslash-escaping an interior quote character. Long + strings are excepted from this analysis under the assumption that their + quote characters are set by policy. + """ + quote_delimiter = _get_quote_delimiter(string_token) + unchosen_delimiter = '"' if quote_delimiter == "'" else "'" + return bool( + quote_delimiter + and not _is_long_string(string_token) + and unchosen_delimiter not in str_eval(string_token) + ) diff --git a/tests/functional/i/inconsistent_quotes.py b/tests/functional/i/inconsistent_quotes.py new file mode 100644 index 00000000000..76553f98be6 --- /dev/null +++ b/tests/functional/i/inconsistent_quotes.py @@ -0,0 +1,16 @@ +"""Tests for inconsistent quoting strategy. + +In this file, double quotes are the majority quote delimiter. +""" + +FIRST_STRING = "double-quoted string" +SECOND_STRING = 'single-quoted string' # [inconsistent-quotes] +THIRD_STRING = "another double-quoted string" +FOURTH_STRING = "yet another double-quoted string" +FIFTH_STRING = 'single-quoted string with an unescaped "double quote"' + +def function_with_docstring(): + '''This is a multi-line docstring that should not raise a warning even though the + delimiter it uses for quotes is not the delimiter used in the majority of the + module. + ''' diff --git a/tests/functional/i/inconsistent_quotes.rc b/tests/functional/i/inconsistent_quotes.rc new file mode 100644 index 00000000000..9935ff9b761 --- /dev/null +++ b/tests/functional/i/inconsistent_quotes.rc @@ -0,0 +1,2 @@ +[STRING] +check-quote-consistency=yes diff --git a/tests/functional/i/inconsistent_quotes.txt b/tests/functional/i/inconsistent_quotes.txt new file mode 100644 index 00000000000..dc5378f145d --- /dev/null +++ b/tests/functional/i/inconsistent_quotes.txt @@ -0,0 +1 @@ +inconsistent-quotes:7::Quote delimiter ' is inconsistent with the rest of the file diff --git a/tests/functional/i/inconsistent_quotes2.py b/tests/functional/i/inconsistent_quotes2.py new file mode 100644 index 00000000000..d9b157e34dc --- /dev/null +++ b/tests/functional/i/inconsistent_quotes2.py @@ -0,0 +1,10 @@ +"""Tests for inconsistent quoting strategy. + +In this file, single quotes are the majority quote delimiter. +""" + +FIRST_STRING = "double-quoted string" # [inconsistent-quotes] +SECOND_STRING = 'single-quoted string' +THIRD_STRING = 'another single-quoted string' +FOURTH_STRING = 'yet another single-quoted string' +FIFTH_STRING = "double-quoted string with an unescaped 'single quote'" diff --git a/tests/functional/i/inconsistent_quotes2.rc b/tests/functional/i/inconsistent_quotes2.rc new file mode 100644 index 00000000000..9935ff9b761 --- /dev/null +++ b/tests/functional/i/inconsistent_quotes2.rc @@ -0,0 +1,2 @@ +[STRING] +check-quote-consistency=yes diff --git a/tests/functional/i/inconsistent_quotes2.txt b/tests/functional/i/inconsistent_quotes2.txt new file mode 100644 index 00000000000..cfed6a83541 --- /dev/null +++ b/tests/functional/i/inconsistent_quotes2.txt @@ -0,0 +1 @@ +inconsistent-quotes:6::Quote delimiter " is inconsistent with the rest of the file From 3c09c846eeeddd21d5755f1220331d744863348e Mon Sep 17 00:00:00 2001 From: Wes Turner Date: Wed, 18 Dec 2019 14:16:48 +0100 Subject: [PATCH 024/240] Makes type hints Python 3.5 compatible. Changing the syntax of a variable's type hint from PEP-526 syntax to the syntax introduced in PEP-484. --- pylint/checkers/strings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylint/checkers/strings.py b/pylint/checkers/strings.py index 85b89fb490f..23a804b6820 100644 --- a/pylint/checkers/strings.py +++ b/pylint/checkers/strings.py @@ -734,7 +734,7 @@ def check_for_consistent_string_delimiters( Args: tokens: The tokens to be checked against for consistent usage. """ - string_delimiters: Counter[str] = collections.Counter() + string_delimiters = collections.Counter() # type: Counter[str] # First, figure out which quote character predominates in the module for tok_type, token, _, _, _ in tokens: From e7e627515135493450712313364cf3fcce325afb Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Sun, 5 Jan 2020 12:32:18 +0100 Subject: [PATCH 025/240] Compile the regular expressions at module time --- pylint/checkers/strings.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pylint/checkers/strings.py b/pylint/checkers/strings.py index 23a804b6820..632696f9613 100644 --- a/pylint/checkers/strings.py +++ b/pylint/checkers/strings.py @@ -66,6 +66,9 @@ "Rb", "RB", } +SINGLE_QUOTED_REGEX = re.compile("(%s)?'''" % "|".join(_PREFIXES)) +DOUBLE_QUOTED_REGEX = re.compile('(%s)?"""' % "|".join(_PREFIXES)) +QUOTE_DELIMITER_REGEX = re.compile("(%s)?(\"|')" % "|".join(_PREFIXES), re.DOTALL) MSGS = { "E1300": ( @@ -890,12 +893,9 @@ def _is_long_string(string_token: str) -> bool: A boolean representing whether or not this token matches a longstring regex. """ - single_quoted_regex = "(%s)?'''" % "|".join(_PREFIXES) - double_quoted_regex = '(%s)?"""' % "|".join(_PREFIXES) - return bool( - re.match(single_quoted_regex, string_token) - or re.match(double_quoted_regex, string_token) + SINGLE_QUOTED_REGEX.match(string_token) + or DOUBLE_QUOTED_REGEX.match(string_token) ) @@ -915,7 +915,7 @@ def _get_quote_delimiter(string_token: str) -> str: Raises: ValueError: No quote delimiter characters are present. """ - match = re.match("(%s)?(\"|')" % "|".join(_PREFIXES), string_token, re.DOTALL) + match = QUOTE_DELIMITER_REGEX.match(string_token) if not match: raise ValueError("string token %s is not a well-formed string" % string_token) return match.group(2) From b3d52631ea19328ab3ff5b2a12c280bc4e63ccaa Mon Sep 17 00:00:00 2001 From: Athos Ribeiro Date: Sun, 5 Jan 2020 23:56:25 +0100 Subject: [PATCH 026/240] Fix false positive for inverse containment tests While dict-keys-not-iterating does not generate false positives for the 'in' containment test, the check does generate false positives for the inverse counterpart of the containment test, 'not in'. This patch changes the check behavior to also consider the 'not in' operator as an iterating context. * Relates to #2186 Signed-off-by: Athos Ribeiro --- CONTRIBUTORS.txt | 3 +++ ChangeLog | 2 ++ pylint/checkers/python3.py | 2 +- tests/unittest_checker_python3.py | 1 + 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index edd19e6fd8f..34e4487f432 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -360,3 +360,6 @@ contributors: * Andy Palmer: contributor * Wes Turner (Google): added new check 'inconsistent-quotes' + +* Athos Ribeiro + Fixed dict-keys-not-iterating false positive for inverse containment checks diff --git a/ChangeLog b/ChangeLog index fc531eb993b..fd60fb47676 100644 --- a/ChangeLog +++ b/ChangeLog @@ -7,6 +7,8 @@ What's New in Pylint 2.5.0? Release date: TBA +* `not in` is considered iterating context for some of the Python 3 porting checkers. + * A new check `inconsistent-quotes` was added. * Add a check for non string assignment to __name__ attribute. diff --git a/pylint/checkers/python3.py b/pylint/checkers/python3.py index 9e248bea93d..65afb8c3afd 100644 --- a/pylint/checkers/python3.py +++ b/pylint/checkers/python3.py @@ -139,7 +139,7 @@ def _in_iterating_context(node): elif ( isinstance(parent, astroid.Compare) and len(parent.ops) == 1 - and parent.ops[0][0] == "in" + and parent.ops[0][0] in ["in", "not in"] ): return True # Also if it's an `yield from`, that's fair diff --git a/tests/unittest_checker_python3.py b/tests/unittest_checker_python3.py index 6eee2530e77..7830092b36b 100644 --- a/tests/unittest_checker_python3.py +++ b/tests/unittest_checker_python3.py @@ -243,6 +243,7 @@ def test_dict_methods_in_iterating_context(self): "max({}())", "min({}())", "3 in {}()", + "3 not in {}()", "set().update({}())", "[].extend({}())", "{{}}.update({}())", From 635e0bd789687f7450fc3cca4b3c3d89ee6c215c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 12 Jan 2020 07:49:21 -0800 Subject: [PATCH 027/240] Simplify pre-commit configuration - no need to quote `types` - the other keys are optional and were their defaults --- .pre-commit-hooks.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index bf6652a7c4f..17d980b4de2 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -2,6 +2,4 @@ name: pylint entry: pylint language: python - 'types': [python] - args: [] - additional_dependencies: [] + types: [python] From b97ceb1f0ef338a1a997d2cde3bab5d81ea15da0 Mon Sep 17 00:00:00 2001 From: Enji Cooper Date: Thu, 9 Jan 2020 16:25:38 -0800 Subject: [PATCH 028/240] Run dos2unix on plugins.rst This will help ensure there are consistent (Unix) file endings. Signed-off-by: Enji Cooper --- doc/how_tos/plugins.rst | 130 ++++++++++++++++++++-------------------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/doc/how_tos/plugins.rst b/doc/how_tos/plugins.rst index 5fc071ed16f..d8f35e71e85 100644 --- a/doc/how_tos/plugins.rst +++ b/doc/how_tos/plugins.rst @@ -1,65 +1,65 @@ -.. -*- coding: utf-8 -*- - -How To Write a Pylint Plugin -============================ - -Pylint provides support for writing two types of extensions. -First, there is the concept of **checkers**, -which can be used for finding problems in your code. -Secondly, there is also the concept of **transform plugin**, -which represents a way through which the inference and -the capabilities of Pylint can be enhanced -and tailored to a particular module, library of framework. - -In general, a plugin is a module which should have a function ``register``, -which takes an instance of ``pylint.lint.PyLinter`` as input. - -A plugin can optionally define also function ``load_configuration``, -which takes an instance of ``pylint.lint.PyLinter`` as input. This -function is called after Pylint loads configuration from configuration -file and command line interface. This function should load additional -plugin specific configuration to Pylint. - -So a basic hello-world plugin can be implemented as: - -.. sourcecode:: python - - # Inside hello_plugin.py - def register(linter): - print 'Hello world' - - -We can run this plugin by placing this module in the PYTHONPATH and invoking -**pylint** as: - -.. sourcecode:: bash - - $ pylint -E --load-plugins hello_plugin foo.py - Hello world - -We can extend hello-world plugin to ignore some specific names using -``load_configuration`` function: - -.. sourcecode:: python - - # Inside hello_plugin.py - def register(linter): - print 'Hello world' - - def load_configuration(linter): - - name_checker = get_checker(linter, NameChecker) - # We consider as good names of variables Hello and World - name_checker.config.good_names += ('Hello', 'World') - - # We ignore bin directory - linter.config.black_list += ('bin',) - -Depending if we need a **transform plugin** or a **checker**, this might not -be enough. For the former, this is enough to declare the module as a plugin, -but in the case of the latter, we need to register our checker with the linter -object, by calling the following inside the ``register`` function:: - - linter.register_checker(OurChecker(linter)) - -For more information on writing a checker see :ref:`write_a_checker`. +.. -*- coding: utf-8 -*- + +How To Write a Pylint Plugin +============================ + +Pylint provides support for writing two types of extensions. +First, there is the concept of **checkers**, +which can be used for finding problems in your code. +Secondly, there is also the concept of **transform plugin**, +which represents a way through which the inference and +the capabilities of Pylint can be enhanced +and tailored to a particular module, library of framework. + +In general, a plugin is a module which should have a function ``register``, +which takes an instance of ``pylint.lint.PyLinter`` as input. + +A plugin can optionally define also function ``load_configuration``, +which takes an instance of ``pylint.lint.PyLinter`` as input. This +function is called after Pylint loads configuration from configuration +file and command line interface. This function should load additional +plugin specific configuration to Pylint. + +So a basic hello-world plugin can be implemented as: + +.. sourcecode:: python + + # Inside hello_plugin.py + def register(linter): + print 'Hello world' + + +We can run this plugin by placing this module in the PYTHONPATH and invoking +**pylint** as: + +.. sourcecode:: bash + + $ pylint -E --load-plugins hello_plugin foo.py + Hello world + +We can extend hello-world plugin to ignore some specific names using +``load_configuration`` function: + +.. sourcecode:: python + + # Inside hello_plugin.py + def register(linter): + print 'Hello world' + + def load_configuration(linter): + + name_checker = get_checker(linter, NameChecker) + # We consider as good names of variables Hello and World + name_checker.config.good_names += ('Hello', 'World') + + # We ignore bin directory + linter.config.black_list += ('bin',) + +Depending if we need a **transform plugin** or a **checker**, this might not +be enough. For the former, this is enough to declare the module as a plugin, +but in the case of the latter, we need to register our checker with the linter +object, by calling the following inside the ``register`` function:: + + linter.register_checker(OurChecker(linter)) + +For more information on writing a checker see :ref:`write_a_checker`. From 8fa14a93ce351942a5c2a6a15306730c2b56baca Mon Sep 17 00:00:00 2001 From: Enji Cooper Date: Thu, 9 Jan 2020 16:26:24 -0800 Subject: [PATCH 029/240] Refine plugin howto * Fix grammar in section describing `load_configuration`. * Make the code python 3 compatible by providing examples with `print` as a function, as opposed to a built-in. Signed-off-by: Enji Cooper --- doc/how_tos/plugins.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/how_tos/plugins.rst b/doc/how_tos/plugins.rst index d8f35e71e85..a68bd22f82d 100644 --- a/doc/how_tos/plugins.rst +++ b/doc/how_tos/plugins.rst @@ -14,7 +14,7 @@ and tailored to a particular module, library of framework. In general, a plugin is a module which should have a function ``register``, which takes an instance of ``pylint.lint.PyLinter`` as input. -A plugin can optionally define also function ``load_configuration``, +A plugin can optionally define a function, ``load_configuration``, which takes an instance of ``pylint.lint.PyLinter`` as input. This function is called after Pylint loads configuration from configuration file and command line interface. This function should load additional @@ -26,7 +26,7 @@ So a basic hello-world plugin can be implemented as: # Inside hello_plugin.py def register(linter): - print 'Hello world' + print('Hello world') We can run this plugin by placing this module in the PYTHONPATH and invoking @@ -44,7 +44,7 @@ We can extend hello-world plugin to ignore some specific names using # Inside hello_plugin.py def register(linter): - print 'Hello world' + print('Hello world') def load_configuration(linter): From a4136c14b98e65e1fe0bab4ec28564700ef983ed Mon Sep 17 00:00:00 2001 From: Enji Cooper Date: Thu, 9 Jan 2020 16:29:30 -0800 Subject: [PATCH 030/240] Add ChangeLog entry for changes made to `doc/how_tos/plugins.rst` Signed-off-by: Enji Cooper --- ChangeLog | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog b/ChangeLog index fd60fb47676..1ad2f0f6845 100644 --- a/ChangeLog +++ b/ChangeLog @@ -7,6 +7,8 @@ What's New in Pylint 2.5.0? Release date: TBA +* Clean up plugin HOWTO documentation. + * `not in` is considered iterating context for some of the Python 3 porting checkers. * A new check `inconsistent-quotes` was added. From be87624a4831162b0ae68f69af986b1597a2d78b Mon Sep 17 00:00:00 2001 From: Anubhav <35621759+anubh-v@users.noreply.github.com> Date: Tue, 14 Jan 2020 16:23:20 +0800 Subject: [PATCH 031/240] BasicChecker: fix typo in 'visit_with' method (#3345) --- CONTRIBUTORS.txt | 2 ++ pylint/checkers/base.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 34e4487f432..2ea3e42f5e0 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -363,3 +363,5 @@ contributors: * Athos Ribeiro Fixed dict-keys-not-iterating false positive for inverse containment checks + +* Anubhav: contributor diff --git a/pylint/checkers/base.py b/pylint/checkers/base.py index 3c7b90249ba..1f611fa9d1a 100644 --- a/pylint/checkers/base.py +++ b/pylint/checkers/base.py @@ -1491,7 +1491,7 @@ def _check_reversed(self, node): @utils.check_messages("confusing-with-statement") def visit_with(self, node): - # a "with" statement with multiple managers coresponds + # a "with" statement with multiple managers corresponds # to one AST "With" node with multiple items pairs = node.items if pairs: From f2f4e6f42416644471ab003d4df7ecf052c3c411 Mon Sep 17 00:00:00 2001 From: Anubhav <35621759+anubh-v@users.noreply.github.com> Date: Wed, 15 Jan 2020 00:06:47 +0800 Subject: [PATCH 032/240] Add a check for asserts on string literals (#3346) This check is emitted whenever **pylint** finds an assert statement with a string literal as its first argument. Such assert statements are probably unintended as they will always pass. Close #3284 --- ChangeLog | 4 ++++ doc/whatsnew/2.5.rst | 6 ++++++ pylint/checkers/base.py | 13 +++++++++++-- tests/functional/a/assert_on_string_literal.py | 3 +++ tests/functional/a/assert_on_string_literal.txt | 1 + 5 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 tests/functional/a/assert_on_string_literal.py create mode 100644 tests/functional/a/assert_on_string_literal.txt diff --git a/ChangeLog b/ChangeLog index 1ad2f0f6845..e9bb34d9e1a 100644 --- a/ChangeLog +++ b/ChangeLog @@ -7,6 +7,10 @@ What's New in Pylint 2.5.0? Release date: TBA +* Add a check for asserts on string literals. + + Close #3284 + * Clean up plugin HOWTO documentation. * `not in` is considered iterating context for some of the Python 3 porting checkers. diff --git a/doc/whatsnew/2.5.rst b/doc/whatsnew/2.5.rst index f37ff257918..fe3bb91edab 100644 --- a/doc/whatsnew/2.5.rst +++ b/doc/whatsnew/2.5.rst @@ -13,6 +13,12 @@ Summary -- Release highlights New checkers ============ +* A new check ``assert-on-string-literal`` was added. + + This check is emitted whenever **pylint** finds an assert statement + with a string literal as its first argument. Such assert statements + are probably unintended as they will always pass. + * A new check ``f-string-without-interpolation`` was added. This check is emitted whenever **pylint** detects the use of an diff --git a/pylint/checkers/base.py b/pylint/checkers/base.py index 1f611fa9d1a..aa6861c2af2 100644 --- a/pylint/checkers/base.py +++ b/pylint/checkers/base.py @@ -1016,6 +1016,12 @@ class BasicChecker(_BasicChecker): 'print("value: {}".format(123)). This might not be what the user ' "intended to do.", ), + "W0129": ( + "Assert statement has a string literal as its first argument. The assert will never fail.", + "assert-on-string-literal", + "Used when an assert statement has a string literal as its first argument, which will " + "cause the assert to always pass.", + ), } reports = (("RP0101", "Statistics by type", report_by_type_stats),) @@ -1377,9 +1383,9 @@ def visit_call(self, node): elif name == "eval": self.add_message("eval-used", node=node) - @utils.check_messages("assert-on-tuple") + @utils.check_messages("assert-on-tuple", "assert-on-string-literal") def visit_assert(self, node): - """check the use of an assert statement on a tuple.""" + """check whether assert is used on a tuple or string literal.""" if ( node.fail is None and isinstance(node.test, astroid.Tuple) @@ -1387,6 +1393,9 @@ def visit_assert(self, node): ): self.add_message("assert-on-tuple", node=node) + if isinstance(node.test, astroid.Const) and isinstance(node.test.value, str): + self.add_message("assert-on-string-literal", node=node) + @utils.check_messages("duplicate-key") def visit_dict(self, node): """check duplicate key in dictionary""" diff --git a/tests/functional/a/assert_on_string_literal.py b/tests/functional/a/assert_on_string_literal.py new file mode 100644 index 00000000000..b5076e87652 --- /dev/null +++ b/tests/functional/a/assert_on_string_literal.py @@ -0,0 +1,3 @@ +# pylint: disable=missing-module-docstring, undefined-variable +assert [foo, bar], "No AssertionError" +assert "There is an AssertionError" # [assert-on-string-literal] diff --git a/tests/functional/a/assert_on_string_literal.txt b/tests/functional/a/assert_on_string_literal.txt new file mode 100644 index 00000000000..ef91deb89d9 --- /dev/null +++ b/tests/functional/a/assert_on_string_literal.txt @@ -0,0 +1 @@ +assert-on-string-literal:3::Assert statement has a string literal as its first argument. The assert will never fail. From aca6d53ab8da456e7d2ba030cbdfc6286942c892 Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Sun, 26 Jan 2020 17:13:11 +0900 Subject: [PATCH 033/240] update examples/pylintrc with new logging-format-style help (#3360) follow-up to #3095 --- examples/pylintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/pylintrc b/examples/pylintrc index fd1befdaf57..da17c23bf1a 100644 --- a/examples/pylintrc +++ b/examples/pylintrc @@ -242,7 +242,7 @@ signature-mutators= [LOGGING] # Format style used to check logging format string. `old` means using % -# formatting, while `new` is for `{}` formatting. +# formatting, `new` is for `{}` formatting, and `fstr` is for f-strings. logging-format-style=old # Logging modules to check that the string format arguments are in logging From 09c94a2dce9e1eaec4d4e7ca81adb70d05970f5d Mon Sep 17 00:00:00 2001 From: Anthony Date: Tue, 4 Feb 2020 11:45:18 -0500 Subject: [PATCH 034/240] Remove import of Counter from types (unneeded and prevents use in python 3.5.3 or lower) (#3380) Type hint updated as well along with importing typing module to satisfy type checking. --- CONTRIBUTORS.txt | 2 ++ ChangeLog | 4 ++++ pylint/checkers/strings.py | 5 +++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 2ea3e42f5e0..eb96651a578 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -365,3 +365,5 @@ contributors: Fixed dict-keys-not-iterating false positive for inverse containment checks * Anubhav: contributor + +* Anthony Tan: contributor diff --git a/ChangeLog b/ChangeLog index e9bb34d9e1a..85a37b56613 100644 --- a/ChangeLog +++ b/ChangeLog @@ -7,6 +7,10 @@ What's New in Pylint 2.5.0? Release date: TBA +* Remove unused import + + Close #3379 + * Add a check for asserts on string literals. Close #3284 diff --git a/pylint/checkers/strings.py b/pylint/checkers/strings.py index 632696f9613..1de4c5cb303 100644 --- a/pylint/checkers/strings.py +++ b/pylint/checkers/strings.py @@ -27,7 +27,8 @@ import numbers import re import tokenize -from typing import Counter, Iterable +import typing +from typing import Iterable import astroid from astroid.arguments import CallSite @@ -737,7 +738,7 @@ def check_for_consistent_string_delimiters( Args: tokens: The tokens to be checked against for consistent usage. """ - string_delimiters = collections.Counter() # type: Counter[str] + string_delimiters = collections.Counter() # type: typing.Counter[str] # First, figure out which quote character predominates in the module for tok_type, token, _, _, _ in tokens: From c49ff2a411ba6207116e2c7f5023d5247da460f0 Mon Sep 17 00:00:00 2001 From: Benjamin Graham Date: Tue, 4 Feb 2020 12:09:43 -0500 Subject: [PATCH 035/240] Fixed writing graphs to relative paths (#3378) While playing around with the 'import-graph' setting, I noticed that attempting to write a graph to a local directory did not work as I would have expected. For example, 'import-graph=docs/graph.dot' would attempt to add 'graph.dot' to '/LOCAL/PATH/docs/docs/'. This was because the 'dot_sourcepath' in this scenario would be set to the combination of the 'storedir' ('/LOCAL/PATH/docs') and the 'outputfile' ('docs/graph.dot'). I am requesting that it be changed to just be 'outputfile' in this scenario. Also, I have removed the instantiation of the 'dotfile' variable because it now has no use Co-authored-by: Claudiu Popa --- CONTRIBUTORS.txt | 3 +++ ChangeLog | 4 +--- pylint/graph.py | 13 +++---------- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index eb96651a578..0b82832b74f 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -366,4 +366,7 @@ contributors: * Anubhav: contributor +* Ben Graham: contributor + * Anthony Tan: contributor + diff --git a/ChangeLog b/ChangeLog index 85a37b56613..6a637e4d84d 100644 --- a/ChangeLog +++ b/ChangeLog @@ -7,9 +7,7 @@ What's New in Pylint 2.5.0? Release date: TBA -* Remove unused import - - Close #3379 +* Fixed graph creation for relative paths * Add a check for asserts on string literals. diff --git a/pylint/graph.py b/pylint/graph.py index 0dc7a1460bd..9e0348144cc 100644 --- a/pylint/graph.py +++ b/pylint/graph.py @@ -71,30 +71,23 @@ def get_source(self): source = property(get_source) - def generate(self, outputfile=None, dotfile=None, mapfile=None): + def generate(self, outputfile=None, mapfile=None): """Generates a graph file. :param str outputfile: filename and path [defaults to graphname.png] - :param str dotfile: filename and path [defaults to graphname.dot] :param str mapfile: filename and path :rtype: str :return: a path to the generated file """ name = self.graphname - if not dotfile: - # if 'outputfile' is a dot file use it as 'dotfile' - if outputfile and outputfile.endswith(".dot"): - dotfile = outputfile - else: - dotfile = "%s.dot" % name if outputfile is not None: - storedir, _, target = target_info_from_filename(outputfile) + _, _, target = target_info_from_filename(outputfile) if target != "dot": pdot, dot_sourcepath = tempfile.mkstemp(".dot", name) os.close(pdot) else: - dot_sourcepath = osp.join(storedir, dotfile) + dot_sourcepath = outputfile else: target = "png" pdot, dot_sourcepath = tempfile.mkstemp(".dot", name) From 594b4a0089786c333e1f0896d049e34503e5172e Mon Sep 17 00:00:00 2001 From: Tyler Thieding Date: Wed, 5 Feb 2020 02:31:55 -0500 Subject: [PATCH 036/240] Fixed broad_try_clause extension to check try/finally statements and to check for nested statements. (#3374) Previously, the broad_try_except extension did not check nested statements. For example, an if/else statement and its body inside of a try clause did not result in a message. Now, the extension checks within sub-nodes (i.e., if, for, while, and with) and counts their statements too. Also, the extension also checks try/finally statements now. --- ChangeLog | 3 ++ pylint/extensions/broad_try_clause.py | 16 ++++++++++- tests/extensions/data/broad_try_clause.py | 35 +++++++++++++++++++++++ tests/extensions/test_broad_try_clause.py | 26 ++++++++++++++--- 4 files changed, 75 insertions(+), 5 deletions(-) diff --git a/ChangeLog b/ChangeLog index 6a637e4d84d..c24d34a246e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -181,6 +181,9 @@ Release date: TBA Close #1603 +* Fixed ``broad_try_clause`` extension to check try/finally statements and to + check for nested statements (e.g., inside of an ``if`` statement). + What's New in Pylint 2.4.4? =========================== Release date: 2019-11-13 diff --git a/pylint/extensions/broad_try_clause.py b/pylint/extensions/broad_try_clause.py index 9a61fb6487d..120d7a80a54 100644 --- a/pylint/extensions/broad_try_clause.py +++ b/pylint/extensions/broad_try_clause.py @@ -6,6 +6,8 @@ """Looks for try/except statements with too much code in the try clause.""" +from astroid.node_classes import For, If, While, With + from pylint import checkers, interfaces @@ -43,8 +45,17 @@ class BroadTryClauseChecker(checkers.BaseChecker): ), ) + def _count_statements(self, try_node): + statement_count = len(try_node.body) + + for body_node in try_node.body: + if isinstance(body_node, (For, If, While, With)): + statement_count += self._count_statements(body_node) + + return statement_count + def visit_tryexcept(self, node): - try_clause_statements = len(node.body) + try_clause_statements = self._count_statements(node) if try_clause_statements > self.config.max_try_statements: msg = "try clause contains {0} statements, expected at most {1}".format( try_clause_statements, self.config.max_try_statements @@ -53,6 +64,9 @@ def visit_tryexcept(self, node): "too-many-try-statements", node.lineno, node=node, args=msg ) + def visit_tryfinally(self, node): + self.visit_tryexcept(node) + def register(linter): """Required method to auto register this checker.""" diff --git a/tests/extensions/data/broad_try_clause.py b/tests/extensions/data/broad_try_clause.py index 4d65302c873..5649023c6b4 100644 --- a/tests/extensions/data/broad_try_clause.py +++ b/tests/extensions/data/broad_try_clause.py @@ -5,6 +5,41 @@ try: # [max-try-statements] value = MY_DICTIONARY["key_one"] value += 1 + print("This one has an except clause only.") +except KeyError: + pass + +try: # [max-try-statements] + value = MY_DICTIONARY["key_one"] + value += 1 + print("This one has an finally clause only.") +finally: + pass + +try: # [max-try-statements] + value = MY_DICTIONARY["key_one"] + value += 1 + print("This one has an except clause...") + print("and also a finally clause!") +except KeyError: + pass +finally: + pass + +try: # [max-try-statements] + if "key_one" in MY_DICTIONARY: + entered_if_body = True + print("This verifies that content inside of an if statement is counted too.") + else: + entered_if_body = False + + while False: + print("This verifies that content inside of a while loop is counted too.") + + for item in []: + print("This verifies that content inside of a for loop is counted too.") + + except KeyError: pass diff --git a/tests/extensions/test_broad_try_clause.py b/tests/extensions/test_broad_try_clause.py index 21b591dc157..11205c3ca95 100644 --- a/tests/extensions/test_broad_try_clause.py +++ b/tests/extensions/test_broad_try_clause.py @@ -34,19 +34,37 @@ def setUpClass(cls): cls._linter.disable("I") def test_broad_try_clause_message(self): - elif_test = osp.join( + broad_try_clause_test = osp.join( osp.dirname(osp.abspath(__file__)), "data", "broad_try_clause.py" ) - self._linter.check([elif_test]) + self._linter.check([broad_try_clause_test]) msgs = self._linter.reporter.messages - self.assertEqual(len(msgs), 1) + self.assertEqual(len(msgs), 4) self.assertEqual(msgs[0].symbol, "too-many-try-statements") self.assertEqual( - msgs[0].msg, "try clause contains 2 statements, expected at most 1" + msgs[0].msg, "try clause contains 3 statements, expected at most 1" ) self.assertEqual(msgs[0].line, 5) + self.assertEqual(msgs[1].symbol, "too-many-try-statements") + self.assertEqual( + msgs[1].msg, "try clause contains 3 statements, expected at most 1" + ) + self.assertEqual(msgs[1].line, 12) + + self.assertEqual(msgs[2].symbol, "too-many-try-statements") + self.assertEqual( + msgs[2].msg, "try clause contains 4 statements, expected at most 1" + ) + self.assertEqual(msgs[2].line, 19) + + self.assertEqual(msgs[3].symbol, "too-many-try-statements") + self.assertEqual( + msgs[3].msg, "try clause contains 7 statements, expected at most 1" + ) + self.assertEqual(msgs[3].line, 29) + if __name__ == "__main__": unittest.main() From ab9e7c37a1a245a72ad9f695e0c7d690af463a3f Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Wed, 5 Feb 2020 09:04:17 +0100 Subject: [PATCH 037/240] Emit ``unused-argument`` for functions that partially uses their argument list before raising an exception. (#3385) Close #3246 --- ChangeLog | 4 ++++ pylint/checkers/utils.py | 13 +++---------- tests/functional/u/unused_argument_py3.py | 5 +++++ tests/functional/u/unused_argument_py3.txt | 3 ++- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/ChangeLog b/ChangeLog index c24d34a246e..873d2cba380 100644 --- a/ChangeLog +++ b/ChangeLog @@ -181,6 +181,10 @@ Release date: TBA Close #1603 +* Emit ``unused-argument`` for functions that partially uses their argument list before raising an exception. + + Close #3246 + * Fixed ``broad_try_clause`` extension to check try/finally statements and to check for nested statements (e.g., inside of an ``if`` statement). diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index 4dbddcc8119..336c380ea19 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -263,16 +263,9 @@ def is_super(node: astroid.node_classes.NodeNG) -> bool: return False -def is_error(node: astroid.node_classes.NodeNG) -> bool: - """return true if the function does nothing but raising an exception""" - raises = False - returns = False - for child_node in node.nodes_of_class((astroid.Raise, astroid.Return)): - if isinstance(child_node, astroid.Raise): - raises = True - if isinstance(child_node, astroid.Return): - returns = True - return raises and not returns +def is_error(node: astroid.scoped_nodes.FunctionDef) -> bool: + """Return true if the given function node only raises an exception""" + return len(node.body) == 1 and isinstance(node.body[0], astroid.Raise) builtins = builtins.__dict__.copy() # type: ignore diff --git a/tests/functional/u/unused_argument_py3.py b/tests/functional/u/unused_argument_py3.py index 65af3e729d7..4d0fd9adc0c 100644 --- a/tests/functional/u/unused_argument_py3.py +++ b/tests/functional/u/unused_argument_py3.py @@ -2,3 +2,8 @@ def func(first, *, second): # [unused-argument, unused-argument] pass + + +def only_raises(first, second=42): # [unused-argument] + if first == 24: + raise ValueError diff --git a/tests/functional/u/unused_argument_py3.txt b/tests/functional/u/unused_argument_py3.txt index f575e43f348..802e328855c 100644 --- a/tests/functional/u/unused_argument_py3.txt +++ b/tests/functional/u/unused_argument_py3.txt @@ -1,2 +1,3 @@ unused-argument:3:func:Unused argument 'first' -unused-argument:3:func:Unused argument 'second' \ No newline at end of file +unused-argument:3:func:Unused argument 'second' +unused-argument:7:only_raises:Unused argument 'second' From 06fa7751aee84fe7f4b5be4354484067a8acb729 Mon Sep 17 00:00:00 2001 From: Benny Date: Wed, 5 Feb 2020 17:37:06 +0100 Subject: [PATCH 038/240] Allow dummy variables for 'redeclared-assigned-name' (#3384) `redeclared-assigned-name` now uses `dummy-variables-rgx` to ignore the variables that match the pattern. Close #3341 --- CONTRIBUTORS.txt | 1 + ChangeLog | 4 ++++ pylint/checkers/base.py | 6 ++++++ tests/functional/r/redeclared_assigned_name.py | 9 +++++++++ tests/functional/r/redeclared_assigned_name.rc | 2 ++ tests/functional/r/redeclared_assigned_name.txt | 1 + 6 files changed, 23 insertions(+) create mode 100644 tests/functional/r/redeclared_assigned_name.rc diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 0b82832b74f..26f49890876 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -370,3 +370,4 @@ contributors: * Anthony Tan: contributor +* Benny Müller: contributor diff --git a/ChangeLog b/ChangeLog index 873d2cba380..567c15e2d3f 100644 --- a/ChangeLog +++ b/ChangeLog @@ -7,6 +7,10 @@ What's New in Pylint 2.5.0? Release date: TBA +* Add `dummy-variables-rgx` option for `_redeclared-assigned-name` check. + + Close #3341 + * Fixed graph creation for relative paths * Add a check for asserts on string literals. diff --git a/pylint/checkers/base.py b/pylint/checkers/base.py index aa6861c2af2..b175da07387 100644 --- a/pylint/checkers/base.py +++ b/pylint/checkers/base.py @@ -1553,6 +1553,10 @@ def _check_self_assigning_variable(self, node): ) def _check_redeclared_assign_name(self, targets): + dummy_variables_rgx = lint_utils.get_global_option( + self, "dummy-variables-rgx", default=None + ) + for target in targets: if not isinstance(target, astroid.Tuple): continue @@ -1562,6 +1566,8 @@ def _check_redeclared_assign_name(self, targets): if isinstance(element, astroid.Tuple): self._check_redeclared_assign_name([element]) elif isinstance(element, astroid.AssignName) and element.name != "_": + if dummy_variables_rgx and dummy_variables_rgx.match(element.name): + return found_names.append(element.name) names = collections.Counter(found_names) diff --git a/tests/functional/r/redeclared_assigned_name.py b/tests/functional/r/redeclared_assigned_name.py index 1027feae200..a2ff124a113 100644 --- a/tests/functional/r/redeclared_assigned_name.py +++ b/tests/functional/r/redeclared_assigned_name.py @@ -7,3 +7,12 @@ for FIRST, (SECOND, FIRST, SECOND) in enumerate(range(5, 10)): # [redeclared-assigned-name] print(SECOND) + +for DUMM, DUMM in enumerate(range(5, 10)): # [redeclared-assigned-name] + print(DUMM) + +for DUMMY, DUMMY in enumerate(range(5, 10)): + print(DUMMY) + +for _, _ in enumerate(range(5, 10)): + print(_) diff --git a/tests/functional/r/redeclared_assigned_name.rc b/tests/functional/r/redeclared_assigned_name.rc new file mode 100644 index 00000000000..3043c52fbfe --- /dev/null +++ b/tests/functional/r/redeclared_assigned_name.rc @@ -0,0 +1,2 @@ +[BASIC] +dummy-variables-rgx=DUMMY diff --git a/tests/functional/r/redeclared_assigned_name.txt b/tests/functional/r/redeclared_assigned_name.txt index 06311f9d423..134a1770f8e 100644 --- a/tests/functional/r/redeclared_assigned_name.txt +++ b/tests/functional/r/redeclared_assigned_name.txt @@ -1,3 +1,4 @@ redeclared-assigned-name:3::Redeclared variable 'FIRST' in assignment redeclared-assigned-name:5::Redeclared variable 'SECOND' in assignment redeclared-assigned-name:8::Redeclared variable 'SECOND' in assignment +redeclared-assigned-name:11::Redeclared variable 'DUMM' in assignment From 4c6c9a7feb944b914a7babb508c9b21c8f3a3341 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Tue, 11 Feb 2020 09:52:31 +0100 Subject: [PATCH 039/240] Remove unneeded Changelog entries --- ChangeLog | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/ChangeLog b/ChangeLog index 567c15e2d3f..8bd4fe84ba8 100644 --- a/ChangeLog +++ b/ChangeLog @@ -17,8 +17,6 @@ Release date: TBA Close #3284 -* Clean up plugin HOWTO documentation. - * `not in` is considered iterating context for some of the Python 3 porting checkers. * A new check `inconsistent-quotes` was added. @@ -124,7 +122,6 @@ Release date: TBA * ``inspect.getargvalues`` is no longer marked as deprecated. - * A new check ``f-string-without-interpolation`` was added Close #3190 @@ -134,14 +131,14 @@ Release date: TBA Close #3183 * ``docparams`` extension supports multiple types in raises sections. + Multiple types can also be separated by commas in all valid sections. Closes #2729 * Allow parallel linting when run under Prospector -* Fixed false positives of ``method-hidden`` when a subclass defines - the method that is being hidden. +* Fixed false positives of ``method-hidden`` when a subclass defines the method that is being hidden. Closes #414 @@ -151,10 +148,6 @@ Release date: TBA Closes #2956 -* Fixes a typo in tests/functional/t/ternary.py - - Closes #3237 - * Pass the actual PyLinter object to sub processes to allow using custom PyLinter classes. @@ -172,12 +165,6 @@ Release date: TBA Pylint no longer outputs a traceback, if a file, read from stdin, contains a syntaxerror. -* Clean up .travis.yml - - Use up to date version of python interpreters - -* Use new release of black 19.10b0 for formating - * Fix uppercase style to disallow 3+ uppercase followed by lowercase. * Fixed ``undefined-variable`` and ``unused-import`` false positives From e792b36af8c889a6c85fc792c6b00c1b88cae828 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Tue, 11 Feb 2020 10:31:55 +0100 Subject: [PATCH 040/240] Do not emit `logging-too-many-args` when `logging-format-interpolation` is emitted (#3395) The intention for `logging-too-many-args` and `logging-too-few-args` is to be emitted when the logging string matches the expected format interpolation. Otherwise `logging-too-many-args` will always be emitted when the logging string does not match the expected format interpolation, especially since the arguments have more bearing for the old style interpolation format, using modulo. Close #3362 --- pylint/checkers/logging.py | 1 + tests/unittest_checker_logging.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pylint/checkers/logging.py b/pylint/checkers/logging.py index 63a95b8b4ab..9f91029d52e 100644 --- a/pylint/checkers/logging.py +++ b/pylint/checkers/logging.py @@ -324,6 +324,7 @@ def _check_format_string(self, node, format_arg): node=node, args=self._format_style_args, ) + return except utils.UnsupportedFormatCharacter as ex: char = format_string[ex.index] self.add_message( diff --git a/tests/unittest_checker_logging.py b/tests/unittest_checker_logging.py index fe813cbe68c..50ba2100fd5 100644 --- a/tests/unittest_checker_logging.py +++ b/tests/unittest_checker_logging.py @@ -148,7 +148,7 @@ def test_fstr_not_new_format_style_matching_arguments(self): def test_modulo_not_fstr_format_style_matching_arguments(self): msg = "logging-format-interpolation" args = ("f-string", "") - with_too_many = True + with_too_many = False self._assert_logging_format_message(msg, "('%s', 1)", args, with_too_many) self._assert_logging_format_message( msg, "('%(named)s', {'named': 1})", args, with_too_many @@ -161,7 +161,7 @@ def test_modulo_not_fstr_format_style_matching_arguments(self): def test_brace_not_fstr_format_style_matching_arguments(self): msg = "logging-format-interpolation" args = ("f-string", "") - with_too_many = True + with_too_many = False self._assert_logging_format_message(msg, "('{}', 1)", args, with_too_many) self._assert_logging_format_message(msg, "('{0}', 1)", args, with_too_many) self._assert_logging_format_message( From 51c646bf70a6e0a86492bfd2ddd1885671d64d67 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Tue, 11 Feb 2020 13:08:01 +0100 Subject: [PATCH 041/240] Do not allow ``python -m pylint ...`` to import user code (#3396) ``python -m pylint ...`` adds the current working directory as the first element of ``sys.path``. This opens up a potential security hole where ``pylint`` will import user level code as long as that code resides in modules having the same name as stdlib or pylint's own modules. Close #3386 --- ChangeLog | 9 +++++++++ pylint/__init__.py | 10 ++++++---- pylint/__main__.py | 12 ++++++++++++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/ChangeLog b/ChangeLog index 8bd4fe84ba8..874300c48ba 100644 --- a/ChangeLog +++ b/ChangeLog @@ -7,6 +7,15 @@ What's New in Pylint 2.5.0? Release date: TBA +* Do not allow ``python -m pylint ...`` to import user code + + ``python -m pylint ...`` adds the current working directory as the first element + of ``sys.path``. This opens up a potential security hole where ``pylint`` will import + user level code as long as that code resides in modules having the same name as stdlib + or pylint's own modules. + + Close #3386 + * Add `dummy-variables-rgx` option for `_redeclared-assigned-name` check. Close #3341 diff --git a/pylint/__init__.py b/pylint/__init__.py index 89809389bd9..54ae25f69b0 100644 --- a/pylint/__init__.py +++ b/pylint/__init__.py @@ -10,14 +10,13 @@ import sys from pylint.__pkginfo__ import version as __version__ -from pylint.checkers.similar import Run as SimilarRun -from pylint.epylint import Run as EpylintRun -from pylint.lint import Run as PylintRun -from pylint.pyreverse.main import Run as PyreverseRun + +# pylint: disable=import-outside-toplevel def run_pylint(): """run pylint""" + from pylint.lint import Run as PylintRun try: PylintRun(sys.argv[1:]) @@ -27,17 +26,20 @@ def run_pylint(): def run_epylint(): """run pylint""" + from pylint.epylint import Run as EpylintRun EpylintRun() def run_pyreverse(): """run pyreverse""" + from pylint.pyreverse.main import Run as PyreverseRun PyreverseRun(sys.argv[1:]) def run_symilar(): """run symilar""" + from pylint.checkers.similar import Run as SimilarRun SimilarRun(sys.argv[1:]) diff --git a/pylint/__main__.py b/pylint/__main__.py index e12309b4dae..dc61b7271e0 100644 --- a/pylint/__main__.py +++ b/pylint/__main__.py @@ -2,6 +2,18 @@ # For details: https://github.com/PyCQA/pylint/blob/master/COPYING #!/usr/bin/env python +import os +import sys + import pylint +# Strip out the current working directory from sys.path. +# Having the working directory in `sys.path` means that `pylint` might +# inadvertently import user code from modules having the same name as +# stdlib or pylint's own modules. +# CPython issue: https://bugs.python.org/issue33053 +if sys.path[0] == "" or sys.path[0] == os.getcwd(): + sys.path.pop(0) + + pylint.run_pylint() From be02bc7651ad6311e9b8a94d673271439b7afc09 Mon Sep 17 00:00:00 2001 From: craig-sh Date: Wed, 12 Feb 2020 02:54:11 -0500 Subject: [PATCH 042/240] Add async def checks for overridden methods (#3392) Close #3355 --- ChangeLog | 4 +++ pylint/checkers/classes.py | 23 +++++++++++++--- .../functional/i/invalid_overridden_method.py | 27 +++++++++++++++---- .../i/invalid_overridden_method.txt | 6 +++-- 4 files changed, 50 insertions(+), 10 deletions(-) diff --git a/ChangeLog b/ChangeLog index 874300c48ba..79ec01807e8 100644 --- a/ChangeLog +++ b/ChangeLog @@ -7,6 +7,10 @@ What's New in Pylint 2.5.0? Release date: TBA +* Emit ``invalid-overridden-method`` for improper async def overrides. + + Close #3355 + * Do not allow ``python -m pylint ...`` to import user code ``python -m pylint ...`` adds the current working directory as the first element diff --git a/pylint/checkers/classes.py b/pylint/checkers/classes.py index d43253467d0..edf39707ccc 100644 --- a/pylint/checkers/classes.py +++ b/pylint/checkers/classes.py @@ -568,9 +568,9 @@ def _has_same_layout_slots(slots, assigned_value): "W0236": ( "Method %r was expected to be %r, found it instead as %r", "invalid-overridden-method", - "Used when we detect that a method was overridden as a property " - "or the other way around, which could result in potential bugs at " - "runtime.", + "Used when we detect that a method was overridden in a way " + "that does not match its base class " + "which could result in potential bugs at runtime.", ), "E0236": ( "Invalid object %r in __slots__, must contain only non empty strings", @@ -1109,6 +1109,23 @@ def _check_invalid_overridden_method(self, function_node, parent_function_node): node=function_node, ) + parent_is_async = isinstance(parent_function_node, astroid.AsyncFunctionDef) + current_is_async = isinstance(function_node, astroid.AsyncFunctionDef) + + if parent_is_async and not current_is_async: + self.add_message( + "invalid-overridden-method", + args=(function_node.name, "async", "non-async",), + node=function_node, + ) + + elif not parent_is_async and current_is_async: + self.add_message( + "invalid-overridden-method", + args=(function_node.name, "non-async", "async",), + node=function_node, + ) + def _check_slots(self, node): if "__slots__" not in node.locals: return diff --git a/tests/functional/i/invalid_overridden_method.py b/tests/functional/i/invalid_overridden_method.py index 2a85f8b22f2..213c05b7d32 100644 --- a/tests/functional/i/invalid_overridden_method.py +++ b/tests/functional/i/invalid_overridden_method.py @@ -9,27 +9,44 @@ def prop(self): pass @abc.abstractmethod - def method(self): + async def async_method(self): pass + @abc.abstractmethod + def method_a(self): + pass + + @abc.abstractmethod + def method_b(self): + pass -class Prop(SuperClass): +class ValidDerived(SuperClass): @property def prop(self): return None - def method(self): + async def async_method(self): + return None + + def method_a(self): pass + def method_b(self): + pass -class NoProp(SuperClass): +class InvalidDerived(SuperClass): def prop(self): # [invalid-overridden-method] return None + def async_method(self): # [invalid-overridden-method] + return None + @property - def method(self): # [invalid-overridden-method] + def method_a(self): # [invalid-overridden-method] return None + async def method_b(self): # [invalid-overridden-method] + return None class Property: diff --git a/tests/functional/i/invalid_overridden_method.txt b/tests/functional/i/invalid_overridden_method.txt index d581125bfc2..9657a401653 100644 --- a/tests/functional/i/invalid_overridden_method.txt +++ b/tests/functional/i/invalid_overridden_method.txt @@ -1,2 +1,4 @@ -invalid-overridden-method:26:NoProp.prop:Method 'prop' was expected to be 'property', found it instead as 'method' -invalid-overridden-method:30:NoProp.method:Method 'method' was expected to be 'method', found it instead as 'property' +invalid-overridden-method:38:InvalidDerived.prop:Method 'prop' was expected to be 'property', found it instead as 'method' +invalid-overridden-method:41:InvalidDerived.async_method:Method 'async_method' was expected to be 'async', found it instead as 'non-async' +invalid-overridden-method:45:InvalidDerived.method_a:Method 'method_a' was expected to be 'method', found it instead as 'property' +invalid-overridden-method:48:InvalidDerived.method_b:Method 'method_b' was expected to be 'non-async', found it instead as 'async' From 5f49ac2835afc1d5d1d19db763b122d51f145cdf Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Wed, 12 Feb 2020 09:11:09 +0100 Subject: [PATCH 043/240] Adjust assert-on-string-literal to take in consideration empty strings (#3403) Close #3400 --- pylint/checkers/base.py | 8 ++++++-- tests/functional/a/assert_on_string_literal.py | 1 + tests/functional/a/assert_on_string_literal.txt | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pylint/checkers/base.py b/pylint/checkers/base.py index b175da07387..c11212befea 100644 --- a/pylint/checkers/base.py +++ b/pylint/checkers/base.py @@ -1017,7 +1017,7 @@ class BasicChecker(_BasicChecker): "intended to do.", ), "W0129": ( - "Assert statement has a string literal as its first argument. The assert will never fail.", + "Assert statement has a string literal as its first argument. The assert will %s fail.", "assert-on-string-literal", "Used when an assert statement has a string literal as its first argument, which will " "cause the assert to always pass.", @@ -1394,7 +1394,11 @@ def visit_assert(self, node): self.add_message("assert-on-tuple", node=node) if isinstance(node.test, astroid.Const) and isinstance(node.test.value, str): - self.add_message("assert-on-string-literal", node=node) + if node.test.value: + when = "never" + else: + when = "always" + self.add_message("assert-on-string-literal", node=node, args=(when,)) @utils.check_messages("duplicate-key") def visit_dict(self, node): diff --git a/tests/functional/a/assert_on_string_literal.py b/tests/functional/a/assert_on_string_literal.py index b5076e87652..14e7d4c556b 100644 --- a/tests/functional/a/assert_on_string_literal.py +++ b/tests/functional/a/assert_on_string_literal.py @@ -1,3 +1,4 @@ # pylint: disable=missing-module-docstring, undefined-variable assert [foo, bar], "No AssertionError" assert "There is an AssertionError" # [assert-on-string-literal] +assert "" # [assert-on-string-literal] diff --git a/tests/functional/a/assert_on_string_literal.txt b/tests/functional/a/assert_on_string_literal.txt index ef91deb89d9..bd3acaeee8d 100644 --- a/tests/functional/a/assert_on_string_literal.txt +++ b/tests/functional/a/assert_on_string_literal.txt @@ -1 +1,2 @@ assert-on-string-literal:3::Assert statement has a string literal as its first argument. The assert will never fail. +assert-on-string-literal:4::Assert statement has a string literal as its first argument. The assert will always fail. From 8956979db811cabc0d96a5f256e3001574e59a00 Mon Sep 17 00:00:00 2001 From: Benny Date: Thu, 13 Feb 2020 09:30:43 +0100 Subject: [PATCH 044/240] Add notes-rgx option for fixme checker (#3394) This commit adds a new `notes-rgx` which is used by the "fixme" check for more granular control over the what fixme messages to emit. Co-authored-by: Claudiu Popa --- ChangeLog | 4 ++++ doc/whatsnew/2.5.rst | 2 ++ examples/pylintrc | 2 ++ man/pylint.1 | 2 ++ pylint/checkers/misc.py | 19 ++++++++++++++++--- tests/functional/f/fixme.py | 9 +++++++++ tests/functional/f/fixme.rc | 3 +++ tests/functional/f/fixme.txt | 5 ++++- 8 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 tests/functional/f/fixme.rc diff --git a/ChangeLog b/ChangeLog index 79ec01807e8..578cc1110b6 100644 --- a/ChangeLog +++ b/ChangeLog @@ -7,6 +7,10 @@ What's New in Pylint 2.5.0? Release date: TBA +* Add 'notes-rgx' option, to be used for fixme check. + + Close #2874 + * Emit ``invalid-overridden-method`` for improper async def overrides. Close #3355 diff --git a/doc/whatsnew/2.5.rst b/doc/whatsnew/2.5.rst index fe3bb91edab..2cb4216fef3 100644 --- a/doc/whatsnew/2.5.rst +++ b/doc/whatsnew/2.5.rst @@ -82,3 +82,5 @@ separated list of regexes, that if a name matches will be always marked as a bla * Add new --fail-under flag for setting the threshold for the score to fail overall tests. If the score is over the fail-under threshold, pylint will complete SystemExit with value 0 to indicate no errors. * Add a new check (non-str-assignment-to-dunder-name) to ensure that only strings are assigned to ``__name__`` attributes + +* Add a new option ``notes-rgx`` to make fixme warnings more flexible. Now either ``notes`` or ``notes-rgx`` option can be used to detect fixme warnings. diff --git a/examples/pylintrc b/examples/pylintrc index da17c23bf1a..7196a820343 100644 --- a/examples/pylintrc +++ b/examples/pylintrc @@ -468,6 +468,8 @@ notes=FIXME, XXX, TODO +# Regular expressio of note tags to take in consideration. +notes-rgx=a^ [STRING] diff --git a/man/pylint.1 b/man/pylint.1 index 40b7c1cbc95..56d5ec3e81c 100644 --- a/man/pylint.1 +++ b/man/pylint.1 @@ -284,6 +284,8 @@ Minimum line length for functions/classes that require docstrings, shorter ones .SH MISCELLANEOUS .IP "--notes=" List of note tags to take in consideration, separated by a comma. [default: FIXME,XXX,TODO] +.IP "--notes-rgx=" +Regular expression of note tags to take in consideration. .SH DESIGN .IP "--max-args=" diff --git a/pylint/checkers/misc.py b/pylint/checkers/misc.py index 18e532514f1..01947b3aafd 100644 --- a/pylint/checkers/misc.py +++ b/pylint/checkers/misc.py @@ -94,13 +94,26 @@ class EncodingChecker(BaseChecker): ), }, ), + ( + "notes-rgx", + { + "type": "string", + "metavar": "", + "help": "Regular expression of note tags to take in consideration.", + }, + ), ) def open(self): super().open() - self._fixme_pattern = re.compile( - r"#\s*(%s)\b" % "|".join(map(re.escape, self.config.notes)), re.I - ) + + notes = "|".join(map(re.escape, self.config.notes)) + if self.config.notes_rgx: + regex_string = r"#\s*(%s|%s)\b" % (notes, self.config.notes_rgx) + else: + regex_string = r"#\s*(%s)\b" % (notes) + + self._fixme_pattern = re.compile(regex_string, re.I) def _check_encoding(self, lineno, line, file_encoding): try: diff --git a/tests/functional/f/fixme.py b/tests/functional/f/fixme.py index 298355d4a1e..081d508f43f 100644 --- a/tests/functional/f/fixme.py +++ b/tests/functional/f/fixme.py @@ -18,6 +18,15 @@ def function(): #FIXME: no space after hash # +1: [fixme] #todo: no space after hash + + # +1: [fixme] + # FIXME: this is broken + # +1: [fixme] + # ./TODO: find with notes + # +1: [fixme] + # TO make something DO: find with regex + # FIXME: this is broken (ISSUE-1234) + #FIXME: in fact nothing to fix #pylint: disable=fixme #TODO: in fact nothing to do #pylint: disable=fixme #TODO: in fact nothing to do #pylint: disable=line-too-long, fixme diff --git a/tests/functional/f/fixme.rc b/tests/functional/f/fixme.rc new file mode 100644 index 00000000000..23d17176884 --- /dev/null +++ b/tests/functional/f/fixme.rc @@ -0,0 +1,3 @@ +[testoptions] +notes=XXX,TODO,./TODO +notes-rgx=FIXME(?!.*ISSUE-\d+)|TO.*DO \ No newline at end of file diff --git a/tests/functional/f/fixme.txt b/tests/functional/f/fixme.txt index d281646e6ce..dfaf67246e3 100644 --- a/tests/functional/f/fixme.txt +++ b/tests/functional/f/fixme.txt @@ -3,4 +3,7 @@ fixme:11::"FIXME: Valid test" fixme:14::"TODO: Do something with the variables" fixme:16::"XXX: Fix this later" fixme:18::"FIXME: no space after hash" -fixme:20::"todo: no space after hash" \ No newline at end of file +fixme:20::"todo: no space after hash" +fixme:23::"FIXME: this is broken" +fixme:25::"./TODO: find with notes" +fixme:27::"TO make something DO: find with regex" \ No newline at end of file From 88895ea8eaf45c7d775f091b068e156848adaabe Mon Sep 17 00:00:00 2001 From: Anubhav <35621759+anubh-v@users.noreply.github.com> Date: Thu, 13 Feb 2020 17:12:07 +0800 Subject: [PATCH 045/240] Add warning for the case where second argument to isinstance is not a type (#3404) The second argument to isinstance must be either a type or a tuple of types. Close #3308 Co-authored-by: Claudiu Popa --- ChangeLog | 5 ++++ doc/whatsnew/2.5.rst | 6 ++++ pylint/checkers/typecheck.py | 26 +++++++++++++++++ .../i/isinstance_second_argument.py | 28 +++++++++++++++++++ .../i/isinstance_second_argument.txt | 4 +++ 5 files changed, 69 insertions(+) create mode 100644 tests/functional/i/isinstance_second_argument.py create mode 100644 tests/functional/i/isinstance_second_argument.txt diff --git a/ChangeLog b/ChangeLog index 578cc1110b6..fe03477cc7b 100644 --- a/ChangeLog +++ b/ChangeLog @@ -7,6 +7,11 @@ What's New in Pylint 2.5.0? Release date: TBA +* Add a check for cases where the second argument to `isinstance` + is not a type. + + Close #3308 + * Add 'notes-rgx' option, to be used for fixme check. Close #2874 diff --git a/doc/whatsnew/2.5.rst b/doc/whatsnew/2.5.rst index 2cb4216fef3..9c41b37a70f 100644 --- a/doc/whatsnew/2.5.rst +++ b/doc/whatsnew/2.5.rst @@ -13,6 +13,12 @@ Summary -- Release highlights New checkers ============ +* A new check ``isinstance-second-argument-not-valid-type``` was added. + + This check is emitted whenever **pylint** finds a call to the `isinstance` + function with a second argument that is not a type. Such code is likely + unintended as it will cause a TypeError to be thrown at runtime error. + * A new check ``assert-on-string-literal`` was added. This check is emitted whenever **pylint** finds an assert statement diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index 92b14a992e5..cc709e95be6 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -395,6 +395,11 @@ def _missing_member_hint(owner, attrname, distance_threshold, max_choices): "non-str-assignment-to-dunder-name", "Emitted when a non-string vaue is assigned to __name__", ), + "W1116": ( + "Second argument of isinstance is not a type", + "isinstance-second-argument-not-valid-type", + "Emitted when the second argument of an isinstance call is not a type.", + ), } # builtin sequence types in Python 2 and 3. @@ -1167,6 +1172,24 @@ def _check_argument_order(self, node, call_site, called, called_param_names): if calling_parg_names != called_param_names[: len(calling_parg_names)]: self.add_message("arguments-out-of-order", node=node, args=()) + def _check_isinstance_args(self, node): + if len(node.args) != 2: + # isinstance called with wrong number of args + return + + def is_not_type(arg): + # Return True if we are sure that arg is not a type + if isinstance(utils.safe_infer(arg), astroid.FunctionDef): + return True + if isinstance(arg, astroid.Tuple): + return any([is_not_type(elt) for elt in arg.elts]) + + return False + + second_arg = node.args[1] + if is_not_type(second_arg): + self.add_message("isinstance-second-argument-not-valid-type", node=node) + # pylint: disable=too-many-branches,too-many-locals @check_messages(*(list(MSGS.keys()))) def visit_call(self, node): @@ -1201,6 +1224,9 @@ def visit_call(self, node): return if called.args.args is None: + if called.name == "isinstance": + # Verify whether second argument of isinstance is a valid type + self._check_isinstance_args(node) # Built-in functions have no argument information. return diff --git a/tests/functional/i/isinstance_second_argument.py b/tests/functional/i/isinstance_second_argument.py new file mode 100644 index 00000000000..71f8fedec21 --- /dev/null +++ b/tests/functional/i/isinstance_second_argument.py @@ -0,0 +1,28 @@ +#pylint: disable=missing-docstring, undefined-variable, invalid-name, too-few-public-methods, wrong-import-position + +# Positive test cases +class A: + pass + +class B(A): + pass + +isinstance(A(), A) +isinstance(A(), B) + +isinstance(-9999, int) +isinstance(True and False, bool) +isinstance("a 'string'", type("test")) + +import collections + +isinstance(3.123213, collections.OrderedDict) +isinstance(foo, (int, collections.Counter)) +isinstance("a string", ((int, type(False)), (float, set), str)) +isinstance(10, (int,) + (str, bool) + (dict, list, tuple)) + +# Negative test cases +isinstance({a:1}, hash) # [isinstance-second-argument-not-valid-type] +isinstance(64, hex) # [isinstance-second-argument-not-valid-type] +isinstance({b: 100}, (hash, dict)) # [isinstance-second-argument-not-valid-type] +isinstance("string", ((dict, iter), str, (int, bool))) # [isinstance-second-argument-not-valid-type] diff --git a/tests/functional/i/isinstance_second_argument.txt b/tests/functional/i/isinstance_second_argument.txt new file mode 100644 index 00000000000..41ba2fd5bf7 --- /dev/null +++ b/tests/functional/i/isinstance_second_argument.txt @@ -0,0 +1,4 @@ +isinstance-second-argument-not-valid-type:25::Second argument of isinstance is not a type +isinstance-second-argument-not-valid-type:26::Second argument of isinstance is not a type +isinstance-second-argument-not-valid-type:27::Second argument of isinstance is not a type +isinstance-second-argument-not-valid-type:28::Second argument of isinstance is not a type From 050421361006043c9d73e7302eb9e3feb62854e0 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Thu, 13 Feb 2020 10:26:18 +0100 Subject: [PATCH 046/240] Allow isinstance-second-argument-not-valid-type to catch more cases --- ChangeLog | 5 ++-- pylint/checkers/typecheck.py | 24 +++++++++++-------- .../i/isinstance_second_argument.py | 1 + .../i/isinstance_second_argument.txt | 1 + 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/ChangeLog b/ChangeLog index fe03477cc7b..9373ddfe015 100644 --- a/ChangeLog +++ b/ChangeLog @@ -7,10 +7,9 @@ What's New in Pylint 2.5.0? Release date: TBA -* Add a check for cases where the second argument to `isinstance` - is not a type. +* Add a check for cases where the second argument to `isinstance` is not a type. - Close #3308 + Close #3308 * Add 'notes-rgx' option, to be used for fixme check. diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index cc709e95be6..7504121275e 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -678,6 +678,19 @@ def _is_c_extension(module_node): ) +def _is_invalid_isinstance_type(arg): + # Return True if we are sure that arg is not a type + inferred = utils.safe_infer(arg) + if not inferred: + # Cannot infer it so skip it. + return False + if isinstance(inferred, astroid.Tuple): + return any(_is_invalid_isinstance_type(elt) for elt in inferred.elts) + if isinstance(inferred, astroid.ClassDef): + return False + return True + + class TypeChecker(BaseChecker): """try to find bugs in the code using type inference """ @@ -1177,17 +1190,8 @@ def _check_isinstance_args(self, node): # isinstance called with wrong number of args return - def is_not_type(arg): - # Return True if we are sure that arg is not a type - if isinstance(utils.safe_infer(arg), astroid.FunctionDef): - return True - if isinstance(arg, astroid.Tuple): - return any([is_not_type(elt) for elt in arg.elts]) - - return False - second_arg = node.args[1] - if is_not_type(second_arg): + if _is_invalid_isinstance_type(second_arg): self.add_message("isinstance-second-argument-not-valid-type", node=node) # pylint: disable=too-many-branches,too-many-locals diff --git a/tests/functional/i/isinstance_second_argument.py b/tests/functional/i/isinstance_second_argument.py index 71f8fedec21..62a721b687b 100644 --- a/tests/functional/i/isinstance_second_argument.py +++ b/tests/functional/i/isinstance_second_argument.py @@ -26,3 +26,4 @@ class B(A): isinstance(64, hex) # [isinstance-second-argument-not-valid-type] isinstance({b: 100}, (hash, dict)) # [isinstance-second-argument-not-valid-type] isinstance("string", ((dict, iter), str, (int, bool))) # [isinstance-second-argument-not-valid-type] +isinstance(int, 1) # [isinstance-second-argument-not-valid-type] diff --git a/tests/functional/i/isinstance_second_argument.txt b/tests/functional/i/isinstance_second_argument.txt index 41ba2fd5bf7..7893d2658f9 100644 --- a/tests/functional/i/isinstance_second_argument.txt +++ b/tests/functional/i/isinstance_second_argument.txt @@ -2,3 +2,4 @@ isinstance-second-argument-not-valid-type:25::Second argument of isinstance is n isinstance-second-argument-not-valid-type:26::Second argument of isinstance is not a type isinstance-second-argument-not-valid-type:27::Second argument of isinstance is not a type isinstance-second-argument-not-valid-type:28::Second argument of isinstance is not a type +isinstance-second-argument-not-valid-type:29::Second argument of isinstance is not a type From bc25b8e32481ac238085b36f45dacfbcfa04dbd4 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Thu, 27 Feb 2020 10:01:04 +0100 Subject: [PATCH 047/240] Allow instances of tuple of the isinstance() type check (#3425) astroid can either infer a tuple call as a `Tuple()` node on successful inference or it can infer the call as an `Instance` of the builtin tuple object, on unsuccessful inference. --- pylint/checkers/typecheck.py | 3 +++ tests/functional/i/isinstance_second_argument.py | 8 +++++--- tests/functional/i/isinstance_second_argument.txt | 4 ++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index 7504121275e..76874085fc6 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -81,6 +81,7 @@ BUILTINS = builtins.__name__ STR_FORMAT = {"%s.str.format" % BUILTINS} ASYNCIO_COROUTINE = "asyncio.coroutines.coroutine" +BUILTIN_TUPLE = "builtins.tuple" def _unflatten(iterable): @@ -688,6 +689,8 @@ def _is_invalid_isinstance_type(arg): return any(_is_invalid_isinstance_type(elt) for elt in inferred.elts) if isinstance(inferred, astroid.ClassDef): return False + if isinstance(inferred, astroid.Instance) and inferred.qname() == BUILTIN_TUPLE: + return False return True diff --git a/tests/functional/i/isinstance_second_argument.py b/tests/functional/i/isinstance_second_argument.py index 62a721b687b..cf32cbfbb55 100644 --- a/tests/functional/i/isinstance_second_argument.py +++ b/tests/functional/i/isinstance_second_argument.py @@ -1,4 +1,7 @@ -#pylint: disable=missing-docstring, undefined-variable, invalid-name, too-few-public-methods, wrong-import-position +#pylint: disable=missing-docstring, undefined-variable, invalid-name, too-few-public-methods, wrong-import-position,import-error + +import collections +from unknown import Unknown # Positive test cases class A: @@ -14,12 +17,11 @@ class B(A): isinstance(True and False, bool) isinstance("a 'string'", type("test")) -import collections - isinstance(3.123213, collections.OrderedDict) isinstance(foo, (int, collections.Counter)) isinstance("a string", ((int, type(False)), (float, set), str)) isinstance(10, (int,) + (str, bool) + (dict, list, tuple)) +isinstance(10, tuple(Unknown)) # Negative test cases isinstance({a:1}, hash) # [isinstance-second-argument-not-valid-type] diff --git a/tests/functional/i/isinstance_second_argument.txt b/tests/functional/i/isinstance_second_argument.txt index 7893d2658f9..7420f90ccfa 100644 --- a/tests/functional/i/isinstance_second_argument.txt +++ b/tests/functional/i/isinstance_second_argument.txt @@ -1,5 +1,5 @@ -isinstance-second-argument-not-valid-type:25::Second argument of isinstance is not a type -isinstance-second-argument-not-valid-type:26::Second argument of isinstance is not a type isinstance-second-argument-not-valid-type:27::Second argument of isinstance is not a type isinstance-second-argument-not-valid-type:28::Second argument of isinstance is not a type isinstance-second-argument-not-valid-type:29::Second argument of isinstance is not a type +isinstance-second-argument-not-valid-type:30::Second argument of isinstance is not a type +isinstance-second-argument-not-valid-type:31::Second argument of isinstance is not a type From 7c559647672e0a73a0bd8744614af1dd62e1b6a1 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Thu, 27 Feb 2020 10:29:33 +0100 Subject: [PATCH 048/240] Skip methods without arguments when generating the dot format. Close #3351 (#3427) --- pylint/pyreverse/writer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pylint/pyreverse/writer.py b/pylint/pyreverse/writer.py index 609b1efcd3d..d1e6f463e2c 100644 --- a/pylint/pyreverse/writer.py +++ b/pylint/pyreverse/writer.py @@ -131,7 +131,10 @@ def get_values(self, obj): if not self.config.only_classnames: label = r"%s|%s\l|" % (label, r"\l".join(obj.attrs)) for func in obj.methods: - args = [arg.name for arg in func.args.args if arg.name != "self"] + if func.args.args: + args = [arg.name for arg in func.args.args if arg.name != "self"] + else: + args = [] label = r"%s%s(%s)\l" % (label, func.name, ", ".join(args)) label = "{%s}" % label if is_exception(obj.node): From a0fc570ce153df978a18fb77bd12915c97744d34 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Fri, 28 Feb 2020 10:37:52 +0100 Subject: [PATCH 049/240] ``typing.overload`` functions are exempted from docstring checks (#3430) Close #3350 --- ChangeLog | 4 ++++ pylint/checkers/base.py | 12 ++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index 9373ddfe015..48ffa649661 100644 --- a/ChangeLog +++ b/ChangeLog @@ -15,6 +15,10 @@ Release date: TBA Close #2874 +* ``typing.overload`` functions are exempted from docstring checks + + Close #3350 + * Emit ``invalid-overridden-method`` for improper async def overrides. Close #3355 diff --git a/pylint/checkers/base.py b/pylint/checkers/base.py index c11212befea..e68a1704348 100644 --- a/pylint/checkers/base.py +++ b/pylint/checkers/base.py @@ -52,7 +52,11 @@ import pylint.utils as lint_utils from pylint import checkers, exceptions, interfaces from pylint.checkers import utils -from pylint.checkers.utils import is_property_deleter, is_property_setter +from pylint.checkers.utils import ( + is_overload_stub, + is_property_deleter, + is_property_setter, +) from pylint.reporters.ureports import nodes as reporter_nodes @@ -2083,7 +2087,11 @@ def visit_classdef(self, node): def visit_functiondef(self, node): if self.config.no_docstring_rgx.match(node.name) is None: ftype = "method" if node.is_method() else "function" - if is_property_setter(node) or is_property_deleter(node): + if ( + is_property_setter(node) + or is_property_deleter(node) + or is_overload_stub(node) + ): return if isinstance(node.parent.frame(), astroid.ClassDef): From eeca79419ea3c00dcaade39735fc10d45d48aa35 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Fri, 28 Feb 2020 11:14:36 +0100 Subject: [PATCH 050/240] `function-redefined`` exempts function redefined on a condition. Close #2410 --- ChangeLog | 4 ++++ pylint/checkers/base.py | 24 +++++++++++++++++++++++- tests/functional/f/function_redefined.py | 11 +++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 48ffa649661..3f70cdf37a8 100644 --- a/ChangeLog +++ b/ChangeLog @@ -15,6 +15,10 @@ Release date: TBA Close #2874 +* ``function-redefined`` exempts function redefined on a condition. + + Close #2410 + * ``typing.overload`` functions are exempted from docstring checks Close #3350 diff --git a/pylint/checkers/base.py b/pylint/checkers/base.py index e68a1704348..2526b71f822 100644 --- a/pylint/checkers/base.py +++ b/pylint/checkers/base.py @@ -845,7 +845,6 @@ def _check_redefinition(self, redeftype, node): node, ) if defined_self is not node and not astroid.are_exclusive(node, defined_self): - # Additional checks for methods which are not considered # redefined, since they are already part of the base API. if ( @@ -854,9 +853,32 @@ def _check_redefinition(self, redeftype, node): ): return + # Skip typing.overload() functions. if utils.is_overload_stub(node): return + # Exempt functions redefined on a condition. + if isinstance(node.parent, astroid.If): + # Exempt "if not " cases + if ( + isinstance(node.parent.test, astroid.UnaryOp) + and node.parent.test.op == "not" + and isinstance(node.parent.test.operand, astroid.Name) + and node.parent.test.operand.name == node.name + ): + return + + # Exempt "if is not None" cases + if ( + isinstance(node.parent.test, astroid.Compare) + and isinstance(node.parent.test.left, astroid.Name) + and node.parent.test.left.name == node.name + and node.parent.test.ops[0][0] == "is" + and isinstance(node.parent.test.ops[0][1], astroid.Const) + and node.parent.test.ops[0][1].value is None + ): + return + # Check if we have forward references for this node. try: redefinition_index = redefinitions.index(node) diff --git a/tests/functional/f/function_redefined.py b/tests/functional/f/function_redefined.py index ba2832894cd..554b68fc9a2 100644 --- a/tests/functional/f/function_redefined.py +++ b/tests/functional/f/function_redefined.py @@ -107,3 +107,14 @@ def __module__(self): @property def __doc__(self): return "Docstring" + + +# Do not emit the error for conditional definitions +def func(callback1=None, callback2=None): + if not callback1: + def callback1(): + return 42 + if callback2 is None: + def callback2(): + return 24 + return callback1(), callback2() From e64b09e880f5a5c43888526fa2e6a4f87636a9fe Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Fri, 28 Feb 2020 11:31:38 +0100 Subject: [PATCH 051/240] Disable too-many-boolean-expressions where it does not make sense --- pylint/checkers/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pylint/checkers/base.py b/pylint/checkers/base.py index 2526b71f822..31db645388a 100644 --- a/pylint/checkers/base.py +++ b/pylint/checkers/base.py @@ -869,6 +869,7 @@ def _check_redefinition(self, redeftype, node): return # Exempt "if is not None" cases + # pylint: disable=too-many-boolean-expressions if ( isinstance(node.parent.test, astroid.Compare) and isinstance(node.parent.test.left, astroid.Name) From 36dcfa879ce38bd4ff489cb04c093db24770c30f Mon Sep 17 00:00:00 2001 From: Gabriel R Sezefredo Date: Sat, 29 Feb 2020 05:13:23 -0300 Subject: [PATCH 052/240] dangerous-default-value accounts for kwargs defaults. Close #3373 (#3423) Signed-off-by: Gabriel R. Sezefredo <--global> --- ChangeLog | 4 ++++ pylint/checkers/base.py | 5 ++++- tests/functional/d/dangerous_default_value_py30.py | 4 ++++ tests/functional/d/dangerous_default_value_py30.txt | 1 + 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 3f70cdf37a8..7d329107707 100644 --- a/ChangeLog +++ b/ChangeLog @@ -7,6 +7,10 @@ What's New in Pylint 2.5.0? Release date: TBA +* Fix dangerous-default-value rule to account for keyword argument defaults + + Close #3373 + * Add a check for cases where the second argument to `isinstance` is not a type. Close #3308 diff --git a/pylint/checkers/base.py b/pylint/checkers/base.py index 31db645388a..5bc63bb965c 100644 --- a/pylint/checkers/base.py +++ b/pylint/checkers/base.py @@ -1292,7 +1292,10 @@ def visit_functiondef(self, node): def _check_dangerous_default(self, node): # check for dangerous default values as arguments is_iterable = lambda n: isinstance(n, (astroid.List, astroid.Set, astroid.Dict)) - for default in node.args.defaults: + defaults = node.args.defaults or [] + node.args.kw_defaults or [] + for default in defaults: + if not default: + continue try: value = next(default.infer()) except astroid.InferenceError: diff --git a/tests/functional/d/dangerous_default_value_py30.py b/tests/functional/d/dangerous_default_value_py30.py index 9aab8163507..4f74dbe266b 100644 --- a/tests/functional/d/dangerous_default_value_py30.py +++ b/tests/functional/d/dangerous_default_value_py30.py @@ -105,3 +105,7 @@ def function22(value=collections.UserDict()): # [dangerous-default-value] def function23(value=collections.UserList()): # [dangerous-default-value] """mutable, dangerous""" return value + +def function24(*, value=[]): # [dangerous-default-value] + """dangerous default value in kwarg.""" + return value diff --git a/tests/functional/d/dangerous_default_value_py30.txt b/tests/functional/d/dangerous_default_value_py30.txt index 40caaf5a33b..135097a9679 100644 --- a/tests/functional/d/dangerous_default_value_py30.txt +++ b/tests/functional/d/dangerous_default_value_py30.txt @@ -19,3 +19,4 @@ dangerous-default-value:93:function20:Dangerous default value OrderedDict() (col dangerous-default-value:97:function21:Dangerous default value defaultdict() (collections.defaultdict) as argument dangerous-default-value:101:function22:Dangerous default value UserDict() (collections.UserDict) as argument dangerous-default-value:105:function23:Dangerous default value UserList() (collections.UserList) as argument +dangerous-default-value:109:function24:Dangerous default value [] as argument From c5faab4caa77c4f5b16f9a411c79c5b7a2e98753 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Sat, 29 Feb 2020 09:19:15 +0100 Subject: [PATCH 053/240] Remove section related to Python 2 support We no longer support Python 2, nor maintain 1.9.X any longer. Close #3431 --- doc/faq.rst | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/doc/faq.rst b/doc/faq.rst index 46cf260c5d3..953e3776dfb 100644 --- a/doc/faq.rst +++ b/doc/faq.rst @@ -50,16 +50,7 @@ supported. 2.4 What versions of Python is Pylint supporting? -------------------------------------------------- -Since Pylint 2.X, the supported running environment is Python 3.5+. - -That is, Pylint 2.X is still able to analyze Python 2 files, but some -specific checks might not work, as they would assume that their running -environment was Python 2. - -If you need to run pylint with Python 2, then Pylint 1.8 or 1.9 is for you. -We will still do backports of bug fixes, and possibly for various Python 3 -compatibility checks, at least until 2020, after which we'll stop support -Python 2 altogether. +The supported running environment since Pylint 2.X is Python 3.5+. 3. Running Pylint From ea13058b9fde38698515c93bf67cc6018ed0064e Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Sat, 29 Feb 2020 10:12:31 +0100 Subject: [PATCH 054/240] Only check the messages that are emitted in VariablesChecker.visit_name --- pylint/checkers/variables.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index bf723a2fa0c..a8cff894099 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -937,11 +937,14 @@ def visit_assignname(self, node): def visit_delname(self, node): self.visit_name(node) - @utils.check_messages(*MSGS) + @utils.check_messages( + "cell-var-from-loop", + "undefined-loop-variable", + "undefined-variable", + "used-before-assignment", + ) def visit_name(self, node): - """check that a name is defined if the current scope and doesn't - redefine a built-in - """ + """Check that a name is defined in the current scope""" stmt = node.statement() if stmt.fromlineno is None: # name node from an astroid built from live code, skip From fb38afb55a6f27f17c113589c406b527dfa5c332 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Sat, 29 Feb 2020 19:13:26 +0100 Subject: [PATCH 055/240] Fix a false positive for ``undefined-variable`` when ``__class__`` is used Close #3090 --- ChangeLog | 4 ++++ pylint/checkers/variables.py | 1 + tests/functional/u/undefined_variable.py | 6 ++++++ 3 files changed, 11 insertions(+) diff --git a/ChangeLog b/ChangeLog index 7d329107707..1459d3109d7 100644 --- a/ChangeLog +++ b/ChangeLog @@ -11,6 +11,10 @@ Release date: TBA Close #3373 +* Fix a false positive for ``undefined-variable`` when ``__class__`` is used + + Close #3090 + * Add a check for cases where the second argument to `isinstance` is not a type. Close #3308 diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index a8cff894099..cc0e1aabb7a 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -1141,6 +1141,7 @@ def visit_name(self, node): name in astroid.Module.scope_attrs or utils.is_builtin(name) or name in self.config.additional_builtins + or (name == "__class__" and isinstance(frame, astroid.FunctionDef)) ): if not utils.node_ignores_exception(node, NameError): self.add_message("undefined-variable", args=name, node=node) diff --git a/tests/functional/u/undefined_variable.py b/tests/functional/u/undefined_variable.py index 4787beb9a21..82059997ddb 100644 --- a/tests/functional/u/undefined_variable.py +++ b/tests/functional/u/undefined_variable.py @@ -281,3 +281,9 @@ def not_using_loop_variable_accordingly(iterator): for iteree in iteree: # [undefined-variable] yield iteree # pylint: enable=unused-argument + + +class DunderClass: + def method(self): + # This name is not defined in the AST but it's present at runtime + return __class__ From 2227ae64697d0d714d782cb24d9b9e8d961075b9 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Mon, 2 Mar 2020 09:06:34 +0100 Subject: [PATCH 056/240] Only exempt __class__ for undefined-variable from methods --- pylint/checkers/variables.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index cc0e1aabb7a..bdf787aee00 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -1141,7 +1141,11 @@ def visit_name(self, node): name in astroid.Module.scope_attrs or utils.is_builtin(name) or name in self.config.additional_builtins - or (name == "__class__" and isinstance(frame, astroid.FunctionDef)) + or ( + name == "__class__" + and isinstance(frame, astroid.FunctionDef) + and frame.is_method() + ) ): if not utils.node_ignores_exception(node, NameError): self.add_message("undefined-variable", args=name, node=node) From b90dfad23b9238f6589ff1a77eda6ce4f28fa768 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Mon, 2 Mar 2020 09:14:14 +0100 Subject: [PATCH 057/240] Emit ``invalid-name`` for variables defined in loops at module level. Close #2695 --- ChangeLog | 4 ++++ pylint/checkers/base.py | 2 +- tests/functional/i/invalid_name.py | 5 +++++ tests/functional/i/invalid_name.txt | 1 + 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 1459d3109d7..873c3487d94 100644 --- a/ChangeLog +++ b/ChangeLog @@ -15,6 +15,10 @@ Release date: TBA Close #3090 +* Emit ``invalid-name`` for variables defined in loops at module level. + + Close #2695 + * Add a check for cases where the second argument to `isinstance` is not a type. Close #3308 diff --git a/pylint/checkers/base.py b/pylint/checkers/base.py index 5bc63bb965c..11c181be8da 100644 --- a/pylint/checkers/base.py +++ b/pylint/checkers/base.py @@ -1931,7 +1931,7 @@ def visit_assignname(self, node): if isinstance(assign_type, astroid.Comprehension): self._check_name("inlinevar", node.name, node) elif isinstance(frame, astroid.Module): - if isinstance(assign_type, astroid.Assign) and not in_loop(assign_type): + if isinstance(assign_type, astroid.Assign): if isinstance(utils.safe_infer(assign_type.value), astroid.ClassDef): self._check_name("class", node.name, node) # Don't emit if the name redefines an import diff --git a/tests/functional/i/invalid_name.py b/tests/functional/i/invalid_name.py index 83dd22360e7..6259b9b50c8 100644 --- a/tests/functional/i/invalid_name.py +++ b/tests/functional/i/invalid_name.py @@ -37,3 +37,8 @@ def _generate_cmdline_tests(): # Valid command only -> valid for item in valid: yield TestCase(''.join(item), True) + + +# We should emit for the loop variable. +for i in range(10): + Foocapfor = 2 # [invalid-name] diff --git a/tests/functional/i/invalid_name.txt b/tests/functional/i/invalid_name.txt index 25d3a56f0d5..94d477b5fa0 100644 --- a/tests/functional/i/invalid_name.txt +++ b/tests/functional/i/invalid_name.txt @@ -1,3 +1,4 @@ invalid-name:10::"Constant name ""aaa"" doesn't conform to UPPER_CASE naming style" invalid-name:14::"Constant name ""time"" doesn't conform to UPPER_CASE naming style" invalid-name:30:a:"Function name ""a"" doesn't conform to snake_case naming style" +invalid-name:44::"Constant name ""Foocapfor"" doesn't conform to UPPER_CASE naming style" From 93bc0dbe2fd7a936826a8dea637bacc552fe2dbe Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Mon, 2 Mar 2020 09:54:08 +0100 Subject: [PATCH 058/240] Protect `node_frame_class` against looping continuously on Uninferable `node_frame_class` was receiving an `Uninferable` object. This object has the property that its boolean value is false, but comparing it against `None` will always return true. Because `node_frame_class` was walking up the tree to find the containing class, it was exempting any node that was not a class, and since comparing `Uninferable` to `None` was always returning true, the loop was running forever. Close #3426 --- pylint/checkers/utils.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index 336c380ea19..ea67814d7bf 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -605,8 +605,16 @@ def node_frame_class(node: astroid.node_classes.NodeNG) -> Optional[astroid.Clas classmethod), otherwise it returns `None`. """ klass = node.frame() - - while klass is not None and not isinstance(klass, astroid.ClassDef): + nodes_to_check = ( + astroid.node_classes.NodeNG, + astroid.UnboundMethod, + astroid.BaseInstance, + ) + while ( + klass + and isinstance(klass, nodes_to_check) + and not isinstance(klass, astroid.ClassDef) + ): if klass.parent is None: klass = None else: From 8ade2da1f057ca6fd9743e8f130f4d67acbf39b6 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Tue, 3 Mar 2020 08:19:00 +0100 Subject: [PATCH 059/240] Fix a false positive of ``self-assigning-variable`` on tuple unpacking. Close #3433 --- ChangeLog | 4 ++++ pylint/checkers/base.py | 3 +++ tests/functional/s/self_assigning_variable.py | 1 + 3 files changed, 8 insertions(+) diff --git a/ChangeLog b/ChangeLog index 873c3487d94..52c2580b489 100644 --- a/ChangeLog +++ b/ChangeLog @@ -11,6 +11,10 @@ Release date: TBA Close #3373 +* Fix a false positive of ``self-assigning-variable`` on tuple unpacking. + + Close #3433 + * Fix a false positive for ``undefined-variable`` when ``__class__`` is used Close #3090 diff --git a/pylint/checkers/base.py b/pylint/checkers/base.py index 11c181be8da..eae308e201a 100644 --- a/pylint/checkers/base.py +++ b/pylint/checkers/base.py @@ -1561,6 +1561,9 @@ def _check_self_assigning_variable(self, node): # A complex assignment, so bail out early. return targets = targets[0].elts + if len(targets) == 1: + # Unpacking a variable into the same name. + return if isinstance(node.value, astroid.Name): if len(targets) != 1: diff --git a/tests/functional/s/self_assigning_variable.py b/tests/functional/s/self_assigning_variable.py index 6df2647e6b2..c9f2781db91 100644 --- a/tests/functional/s/self_assigning_variable.py +++ b/tests/functional/s/self_assigning_variable.py @@ -21,6 +21,7 @@ class Class: FOO = 1 +FOO, = [FOO] class RedefinedModuleLevel: From 121e20424cb93d4431d8603bb3fd3640c9e8c817 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Tue, 3 Mar 2020 08:21:06 +0100 Subject: [PATCH 060/240] Remove superfluous check which is already done on the next line --- pylint/checkers/base.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pylint/checkers/base.py b/pylint/checkers/base.py index eae308e201a..7eb35be0b58 100644 --- a/pylint/checkers/base.py +++ b/pylint/checkers/base.py @@ -2180,8 +2180,6 @@ def _check_docstring( func.bound, astroid.Instance ): # Strings. - if func.bound.name == "str": - return if func.bound.name in ("str", "unicode", "bytes"): return if node_type == "module": From 8d6a4717f35f10e5b78e7133dcf43c44d12c8f21 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Fri, 6 Mar 2020 10:57:27 +0100 Subject: [PATCH 061/240] Add regression test for property false positive. Close #3231 --- .../r/regression_3231_no_member_property.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 tests/functional/r/regression_3231_no_member_property.py diff --git a/tests/functional/r/regression_3231_no_member_property.py b/tests/functional/r/regression_3231_no_member_property.py new file mode 100644 index 00000000000..6cc9cd71cdf --- /dev/null +++ b/tests/functional/r/regression_3231_no_member_property.py @@ -0,0 +1,20 @@ +# pylint: disable=missing-docstring +from abc import ABCMeta, abstractmethod + + +class Cls(metaclass=ABCMeta): + def __init__(self): + pass + + @property + @abstractmethod + def values(self): + pass + + @classmethod + def some_method(cls): + return cls.values.issubset({2, 3}) + + +class Subcls(Cls): + values = {1, 2, 3} From e964c89cd6e0296b4c52e5b5b316b957dc6a21f8 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Sat, 7 Mar 2020 10:36:34 +0100 Subject: [PATCH 062/240] ``no-self-use`` is no longer emitted for typing stubs. Close #3439 --- ChangeLog | 4 ++++ pylint/checkers/classes.py | 2 ++ tests/functional/t/typing_use.py | 16 +++++++++++++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 52c2580b489..af5c2b978e4 100644 --- a/ChangeLog +++ b/ChangeLog @@ -15,6 +15,10 @@ Release date: TBA Close #3433 +* ``no-self-use`` is no longer emitted for typing stubs. + + Close #3439 + * Fix a false positive for ``undefined-variable`` when ``__class__`` is used Close #3090 diff --git a/pylint/checkers/classes.py b/pylint/checkers/classes.py index edf39707ccc..4f62f18675e 100644 --- a/pylint/checkers/classes.py +++ b/pylint/checkers/classes.py @@ -52,6 +52,7 @@ is_builtin_object, is_comprehension, is_iterable, + is_overload_stub, is_property_setter, is_property_setter_or_deleter, is_protocol_class, @@ -1207,6 +1208,7 @@ def leave_functiondef(self, node): or decorated_with_property(node) or _has_bare_super_call(node) or is_protocol_class(class_node) + or is_overload_stub(node) ) ): self.add_message("no-self-use", node=node) diff --git a/tests/functional/t/typing_use.py b/tests/functional/t/typing_use.py index 3e2c48ba0a6..68a48d3223a 100644 --- a/tests/functional/t/typing_use.py +++ b/tests/functional/t/typing_use.py @@ -1,7 +1,7 @@ # pylint: disable=missing-docstring import typing - +from typing import overload @typing.overload def double_with_docstring(arg: str) -> str: @@ -49,3 +49,17 @@ def double_with_pass(arg: int) -> int: def double_with_pass(arg): return 2 * arg + +# pylint: disable=too-few-public-methods +class Cls: + @typing.overload + def method(self, param: int) -> None: + ... + + @overload + def method(self, param: str) -> None: + ... + + def method(self, param): + return (self, param) +# pylint: enable=too-few-public-methods \ No newline at end of file From ce58d6e87ac1588311699a30aeb16b60a041fb23 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Sun, 8 Mar 2020 18:06:03 +0100 Subject: [PATCH 063/240] Protect against `exc` not having `pytype` `pytype` is not defined on all nodes, but line 1175 assumed the result of `safe_infer()` might have it. --- pylint/checkers/refactoring.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylint/checkers/refactoring.py b/pylint/checkers/refactoring.py index 28313439a47..4fd9884713b 100644 --- a/pylint/checkers/refactoring.py +++ b/pylint/checkers/refactoring.py @@ -1170,7 +1170,7 @@ def _is_node_return_ended(self, node): #  to infer it. return True exc = utils.safe_infer(node.exc) - if exc is None or exc is astroid.Uninferable: + if exc is None or exc is astroid.Uninferable or not hasattr(exc, "pytype"): return False exc_name = exc.pytype().split(".")[-1] handlers = utils.get_exception_handlers(node, exc_name) From ca87a2c6e181a85cd81a7840e7f1551e511d08db Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Sun, 8 Mar 2020 18:08:29 +0100 Subject: [PATCH 064/240] Protect against passing a non-class to _check_exception_inherit_from_stopiteration --- pylint/checkers/refactoring.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylint/checkers/refactoring.py b/pylint/checkers/refactoring.py index 4fd9884713b..013cedcd5c9 100644 --- a/pylint/checkers/refactoring.py +++ b/pylint/checkers/refactoring.py @@ -649,7 +649,7 @@ def _check_stop_iteration_inside_generator(self, node): if not node.exc: return exc = utils.safe_infer(node.exc) - if exc is None or exc is astroid.Uninferable: + if not exc or not isinstance(exc, (astroid.Instance, astroid.ClassDef)): return if self._check_exception_inherit_from_stopiteration(exc): self.add_message("stop-iteration-return", node=node) From 018e4b669f60dd905c5b86fbe3c963f13c2ce4fa Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Sun, 8 Mar 2020 18:15:04 +0100 Subject: [PATCH 065/240] Correct some types and guard against unexpected values in classes checkers --- pylint/checkers/base.py | 9 ++++++--- pylint/checkers/classes.py | 2 +- pylint/checkers/utils.py | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/pylint/checkers/base.py b/pylint/checkers/base.py index 7eb35be0b58..22eb9b3b33d 100644 --- a/pylint/checkers/base.py +++ b/pylint/checkers/base.py @@ -317,11 +317,10 @@ def _get_properties(config): return property_classes, property_names -def _determine_function_name_type(node, config=None): +def _determine_function_name_type(node: astroid.FunctionDef, config=None): """Determine the name type whose regex the a function's name should match. :param node: A function node. - :type node: astroid.node_classes.NodeNG :param config: Configuration from which to pull additional property classes. :type config: :class:`optparse.Values` @@ -349,7 +348,11 @@ def _determine_function_name_type(node, config=None): and decorator.attrname in property_names ): inferred = utils.safe_infer(decorator) - if inferred and inferred.qname() in property_classes: + if ( + inferred + and hasattr(inferred, "qname") + and inferred.qname() in property_classes + ): return "attr" return "method" diff --git a/pylint/checkers/classes.py b/pylint/checkers/classes.py index 4f62f18675e..af5a0d4e38b 100644 --- a/pylint/checkers/classes.py +++ b/pylint/checkers/classes.py @@ -791,7 +791,7 @@ def _check_proper_bases(self, node): """ for base in node.bases: ancestor = safe_infer(base) - if ancestor in (astroid.Uninferable, None): + if not ancestor: continue if isinstance(ancestor, astroid.Instance) and ancestor.is_subtype_of( "%s.type" % (BUILTINS,) diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index ea67814d7bf..e7c13393301 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -776,7 +776,7 @@ def decorated_with( @lru_cache(maxsize=1024) def unimplemented_abstract_methods( - node: astroid.node_classes.NodeNG, is_abstract_cb: astroid.FunctionDef = None + node: astroid.ClassDef, is_abstract_cb: astroid.FunctionDef = None ) -> Dict[str, astroid.node_classes.NodeNG]: """ Get the unimplemented abstract methods for the given *node*. From 78adea9f4485660eb14dc1c6954de445e76ba66d Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Sun, 8 Mar 2020 18:27:00 +0100 Subject: [PATCH 066/240] Adjust some annotations and refactor small checks --- pylint/checkers/utils.py | 23 ++++++++++++----------- pylint/checkers/variables.py | 2 +- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index e7c13393301..9543bf8f163 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -331,10 +331,13 @@ def is_defined_in_scope( return False -def is_defined_before(var_node: astroid.node_classes.NodeNG) -> bool: - """return True if the variable node is defined by a parent node (list, - set, dict, or generator comprehension, lambda) or in a previous sibling - node on the same line (statement_defining ; statement_using) +def is_defined_before(var_node: astroid.Name) -> bool: + """Check if the given variable node is defined before + + Verify that the variable node is defined by a parent node + (list, set, dict, or generator comprehension, lambda) + or in a previous sibling node on the same line + (statement_defining ; statement_using). """ varname = var_node.name _node = var_node.parent @@ -386,16 +389,14 @@ def is_func_decorator(node: astroid.node_classes.NodeNG) -> bool: def is_ancestor_name( - frame: astroid.node_classes.NodeNG, node: astroid.node_classes.NodeNG + frame: astroid.ClassDef, node: astroid.node_classes.NodeNG ) -> bool: """return True if `frame` is an astroid.Class node with `node` in the subtree of its bases attribute """ - try: - bases = frame.bases - except AttributeError: + if not isinstance(frame, astroid.ClassDef): return False - for base in bases: + for base in frame.bases: if node in base.nodes_of_class(astroid.Name): return True return False @@ -409,7 +410,7 @@ def assign_parent(node: astroid.node_classes.NodeNG) -> astroid.node_classes.Nod return node -def overrides_a_method(class_node: astroid.node_classes.NodeNG, name: str) -> bool: +def overrides_a_method(class_node: astroid.ClassDef, name: str) -> bool: """return True if is a method overridden from an ancestor""" for ancestor in class_node.ancestors(): if name in ancestor and isinstance(ancestor[name], astroid.FunctionDef): @@ -836,7 +837,7 @@ def unimplemented_abstract_methods( def find_try_except_wrapper_node( node: astroid.node_classes.NodeNG, -) -> Union[astroid.ExceptHandler, astroid.TryExcept]: +) -> Optional[Union[astroid.ExceptHandler, astroid.TryExcept]]: """Return the ExceptHandler or the TryExcept node in which the node is.""" current = node ignores = (astroid.ExceptHandler, astroid.TryExcept) diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index bdf787aee00..dc546ccb8d7 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -1751,7 +1751,7 @@ def _check_self_cls_assign(self, node): if isinstance(target, astroid.node_classes.AssignName) ) if self_cls_name in target_assign_names: - self.add_message("self-cls-assignment", node=node, args=(self_cls_name)) + self.add_message("self-cls-assignment", node=node, args=(self_cls_name,)) def _check_unpacking(self, inferred, node, targets): """ Check for unbalanced tuple unpacking From 762e1a13601f5e205a6be1c8b5450f2c519b87de Mon Sep 17 00:00:00 2001 From: bernie gray Date: Wed, 11 Mar 2020 09:13:26 -0400 Subject: [PATCH 067/240] Allow non-ASCII characters in identifiers in the invalid-name rule and add non-ascii-name check Non-ASCII characters are now allowed by ``invalid-name`` check. Also this commit adds a new check ``non-ascii-name``, which is used to detect identifiers with non-ASCII characters. --- CONTRIBUTORS.txt | 2 + ChangeLog | 4 + doc/whatsnew/2.5.rst | 6 +- pylint/checkers/base.py | 89 ++++++++++++------- pylint/pyreverse/utils.py | 10 +-- tests/functional/n/namePresetCamelCase.txt | 6 +- .../n/name_good_bad_names_regex.txt | 2 +- tests/functional/n/name_preset_snake_case.txt | 6 +- tests/functional/n/non_ascii_name.py | 6 ++ tests/functional/n/non_ascii_name.txt | 2 + tests/test_functional.py | 7 +- tests/unittest_checker_base.py | 8 +- 12 files changed, 98 insertions(+), 50 deletions(-) create mode 100644 tests/functional/n/non_ascii_name.py create mode 100644 tests/functional/n/non_ascii_name.txt diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 26f49890876..e05877b1f2b 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -371,3 +371,5 @@ contributors: * Anthony Tan: contributor * Benny Müller: contributor + +* Bernie Gray: contributor diff --git a/ChangeLog b/ChangeLog index af5c2b978e4..81750fae009 100644 --- a/ChangeLog +++ b/ChangeLog @@ -7,6 +7,10 @@ What's New in Pylint 2.5.0? Release date: TBA +* Adjust the `invalid-name` rule to work with non-ASCII identifiers and add the `non-ascii-name` rule. + + Close #2725 + * Fix dangerous-default-value rule to account for keyword argument defaults Close #3373 diff --git a/doc/whatsnew/2.5.rst b/doc/whatsnew/2.5.rst index 9c41b37a70f..3fb98236d1f 100644 --- a/doc/whatsnew/2.5.rst +++ b/doc/whatsnew/2.5.rst @@ -89,4 +89,8 @@ separated list of regexes, that if a name matches will be always marked as a bla * Add a new check (non-str-assignment-to-dunder-name) to ensure that only strings are assigned to ``__name__`` attributes -* Add a new option ``notes-rgx`` to make fixme warnings more flexible. Now either ``notes`` or ``notes-rgx`` option can be used to detect fixme warnings. +* Add a new option ``notes-rgx`` to make fixme warnings more flexible. Now either ``notes`` or ``notes-rgx`` option can be used to detect fixme warnings. + +* Non-ASCII characters are now allowed by ``invalid-name``. + +* Add a new check ``non-ascii-name`` to detect identifiers with non-ASCII characters. diff --git a/pylint/checkers/base.py b/pylint/checkers/base.py index 22eb9b3b33d..9b0ead80d12 100644 --- a/pylint/checkers/base.py +++ b/pylint/checkers/base.py @@ -91,47 +91,47 @@ def get_regex(cls, name_type): class SnakeCaseStyle(NamingStyle): """Regex rules for snake_case naming style.""" - CLASS_NAME_RGX = re.compile("[a-z_][a-z0-9_]+$") - MOD_NAME_RGX = re.compile("[a-z_][a-z0-9_]*$") - CONST_NAME_RGX = re.compile("([a-z_][a-z0-9_]*|__.*__)$") - COMP_VAR_RGX = re.compile("[a-z_][a-z0-9_]*$") + CLASS_NAME_RGX = re.compile(r"[^\W\dA-Z][^\WA-Z]+$") + MOD_NAME_RGX = re.compile(r"[^\W\dA-Z][^\WA-Z]*$") + CONST_NAME_RGX = re.compile(r"([^\W\dA-Z][^\WA-Z]*|__.*__)$") + COMP_VAR_RGX = re.compile(r"[^\W\dA-Z][^\WA-Z]*$") DEFAULT_NAME_RGX = re.compile( - "([a-z_][a-z0-9_]{2,}|_[a-z0-9_]*|__[a-z][a-z0-9_]+__)$" + r"([^\W\dA-Z][^\WA-Z]{2,}|_[^\WA-Z]*|__[^\WA-Z\d_][^\WA-Z]+__)$" ) - CLASS_ATTRIBUTE_RGX = re.compile(r"([a-z_][a-z0-9_]{2,}|__.*__)$") + CLASS_ATTRIBUTE_RGX = re.compile(r"([^\W\dA-Z][^\WA-Z]{2,}|__.*__)$") class CamelCaseStyle(NamingStyle): """Regex rules for camelCase naming style.""" - CLASS_NAME_RGX = re.compile("[a-z_][a-zA-Z0-9]+$") - MOD_NAME_RGX = re.compile("[a-z_][a-zA-Z0-9]*$") - CONST_NAME_RGX = re.compile("([a-z_][A-Za-z0-9]*|__.*__)$") - COMP_VAR_RGX = re.compile("[a-z_][A-Za-z0-9]*$") - DEFAULT_NAME_RGX = re.compile("([a-z_][a-zA-Z0-9]{2,}|__[a-z][a-zA-Z0-9_]+__)$") - CLASS_ATTRIBUTE_RGX = re.compile(r"([a-z_][A-Za-z0-9]{2,}|__.*__)$") + CLASS_NAME_RGX = re.compile(r"[^\W\dA-Z][^\W_]+$") + MOD_NAME_RGX = re.compile(r"[^\W\dA-Z][^\W_]*$") + CONST_NAME_RGX = re.compile(r"([^\W\dA-Z][^\W_]*|__.*__)$") + COMP_VAR_RGX = re.compile(r"[^\W\dA-Z][^\W_]*$") + DEFAULT_NAME_RGX = re.compile(r"([^\W\dA-Z][^\W_]{2,}|__[^\W\dA-Z_]\w+__)$") + CLASS_ATTRIBUTE_RGX = re.compile(r"([^\W\dA-Z][^\W_]{2,}|__.*__)$") class PascalCaseStyle(NamingStyle): """Regex rules for PascalCase naming style.""" - CLASS_NAME_RGX = re.compile("[A-Z_][a-zA-Z0-9]+$") - MOD_NAME_RGX = re.compile("[A-Z_][a-zA-Z0-9]+$") - CONST_NAME_RGX = re.compile("([A-Z_][A-Za-z0-9]*|__.*__)$") - COMP_VAR_RGX = re.compile("[A-Z_][a-zA-Z0-9]+$") - DEFAULT_NAME_RGX = re.compile("([A-Z_][a-zA-Z0-9]{2,}|__[a-z][a-zA-Z0-9_]+__)$") - CLASS_ATTRIBUTE_RGX = re.compile("[A-Z_][a-zA-Z0-9]{2,}$") + CLASS_NAME_RGX = re.compile(r"[^\W\da-z][^\W_]+$") + MOD_NAME_RGX = re.compile(r"[^\W\da-z][^\W_]+$") + CONST_NAME_RGX = re.compile(r"([^\W\da-z][^\W_]*|__.*__)$") + COMP_VAR_RGX = re.compile(r"[^\W\da-z][^\W_]+$") + DEFAULT_NAME_RGX = re.compile(r"([^\W\da-z][^\W_]{2,}|__[^\W\dA-Z_]\w+__)$") + CLASS_ATTRIBUTE_RGX = re.compile(r"[^\W\da-z][^\W_]{2,}$") class UpperCaseStyle(NamingStyle): """Regex rules for UPPER_CASE naming style.""" - CLASS_NAME_RGX = re.compile("[A-Z_][A-Z0-9_]+$") - MOD_NAME_RGX = re.compile("[A-Z_][A-Z0-9_]+$") - CONST_NAME_RGX = re.compile("([A-Z_][A-Z0-9_]*|__.*__)$") - COMP_VAR_RGX = re.compile("[A-Z_][A-Z0-9_]+$") - DEFAULT_NAME_RGX = re.compile("([A-Z_][A-Z0-9_]{2,}|__[a-z][a-zA-Z0-9_]+__)$") - CLASS_ATTRIBUTE_RGX = re.compile("[A-Z_][A-Z0-9_]{2,}$") + CLASS_NAME_RGX = re.compile(r"[^\W\da-z][^\Wa-z]+$") + MOD_NAME_RGX = re.compile(r"[^\W\da-z][^\Wa-z]+$") + CONST_NAME_RGX = re.compile(r"([^\W\da-z][^\Wa-z]*|__.*__)$") + COMP_VAR_RGX = re.compile(r"[^\W\da-z][^\Wa-z]+$") + DEFAULT_NAME_RGX = re.compile(r"([^\W\da-z][^\Wa-z]{2,}|__[^\W\dA-Z_]\w+__)$") + CLASS_ATTRIBUTE_RGX = re.compile(r"[^\W\da-z][^\Wa-z]{2,}$") class AnyStyle(NamingStyle): @@ -1716,6 +1716,11 @@ class NameChecker(_BasicChecker): "Used when the name doesn't conform to naming rules " "associated to its type (constant, variable, class...).", ), + "C0144": ( + '%s name "%s" contains a non-ASCII unicode character', + "non-ascii-name", + "Used when the name contains at least one non-ASCII unciode character.", + ), "W0111": ( "Name %s will become a keyword in Python %s", "assign-to-new-keyword", @@ -1812,6 +1817,7 @@ def __init__(self, linter): self._name_hints = {} self._good_names_rgxs_compiled = [] self._bad_names_rgxs_compiled = [] + self._non_ascii_rgx_compiled = re.compile("[^\u0000-\u007F]") def open(self): self.stats = self.linter.add_stats( @@ -1862,7 +1868,7 @@ def _create_naming_rules(self): return regexps, hints - @utils.check_messages("blacklisted-name", "invalid-name") + @utils.check_messages("blacklisted-name", "invalid-name", "non-ascii-name") def visit_module(self, node): self._check_name("module", node.name.split(".")[-1], node) self._bad_names = {} @@ -1887,7 +1893,9 @@ def leave_module(self, node): # pylint: disable=unused-argument for args in warnings: self._raise_name_warning(*args) - @utils.check_messages("blacklisted-name", "invalid-name", "assign-to-new-keyword") + @utils.check_messages( + "blacklisted-name", "invalid-name", "assign-to-new-keyword", "non-ascii-name" + ) def visit_classdef(self, node): self._check_assign_to_new_keyword_violation(node.name, node) self._check_name("class", node.name, node) @@ -1895,7 +1903,9 @@ def visit_classdef(self, node): if not any(node.instance_attr_ancestors(attr)): self._check_name("attr", attr, anodes[0]) - @utils.check_messages("blacklisted-name", "invalid-name", "assign-to-new-keyword") + @utils.check_messages( + "blacklisted-name", "invalid-name", "assign-to-new-keyword", "non-ascii-name" + ) def visit_functiondef(self, node): # Do not emit any warnings if the method is just an implementation # of a base class method. @@ -1923,12 +1933,14 @@ def visit_functiondef(self, node): visit_asyncfunctiondef = visit_functiondef - @utils.check_messages("blacklisted-name", "invalid-name") + @utils.check_messages("blacklisted-name", "invalid-name", "non-ascii-name") def visit_global(self, node): for name in node.names: self._check_name("const", name, node) - @utils.check_messages("blacklisted-name", "invalid-name", "assign-to-new-keyword") + @utils.check_messages( + "blacklisted-name", "invalid-name", "assign-to-new-keyword", "non-ascii-name" + ) def visit_assignname(self, node): """check module level assigned names""" self._check_assign_to_new_keyword_violation(node.name, node) @@ -1968,14 +1980,20 @@ def _recursive_check_names(self, args, node): def _find_name_group(self, node_type): return self._name_group.get(node_type, node_type) - def _raise_name_warning(self, node, node_type, name, confidence): + def _raise_name_warning( + self, node, node_type, name, confidence, warning="invalid-name" + ): type_label = HUMAN_READABLE_TYPES[node_type] hint = self._name_hints[node_type] if self.config.include_naming_hint: hint += " (%r pattern)" % self._name_regexps[node_type].pattern - args = (type_label.capitalize(), name, hint) + args = ( + (type_label.capitalize(), name, hint) + if warning == "invalid-name" + else (type_label.capitalize(), name) + ) - self.add_message("invalid-name", node=node, args=args, confidence=confidence) + self.add_message(warning, node=node, args=args, confidence=confidence) self.stats["badname_" + node_type] += 1 def _name_valid_due_to_whitelist(self, name: str) -> bool: @@ -1991,6 +2009,13 @@ def _name_invalid_due_to_blacklist(self, name: str) -> bool: def _check_name(self, node_type, name, node, confidence=interfaces.HIGH): """check for a name using the type's regexp""" + non_ascii_match = self._non_ascii_rgx_compiled.match(name) + + if non_ascii_match is not None: + self._raise_name_warning( + node, node_type, name, confidence, warning="non-ascii-name" + ) + def _should_exempt_from_invalid_name(node): if node_type == "variable": inferred = utils.safe_infer(node) diff --git a/pylint/pyreverse/utils.py b/pylint/pyreverse/utils.py index 5a1e7e2668e..471a61ab788 100644 --- a/pylint/pyreverse/utils.py +++ b/pylint/pyreverse/utils.py @@ -48,9 +48,9 @@ def insert_default_options(): # astroid utilities ########################################################### -SPECIAL = re.compile("^__[A-Za-z0-9]+[A-Za-z0-9_]*__$") -PRIVATE = re.compile("^__[_A-Za-z0-9]*[A-Za-z0-9]+_?$") -PROTECTED = re.compile("^_[_A-Za-z0-9]*$") +SPECIAL = re.compile(r"^__[^\W_]+\w*__$") +PRIVATE = re.compile(r"^__\w*[^\W_]+_?$") +PROTECTED = re.compile(r"^_\w*$") def get_visibility(name): @@ -68,8 +68,8 @@ def get_visibility(name): return visibility -ABSTRACT = re.compile("^.*Abstract.*") -FINAL = re.compile("^[A-Z_]*$") +ABSTRACT = re.compile(r"^.*Abstract.*") +FINAL = re.compile(r"^[^\W\da-z]*$") def is_abstract(node): diff --git a/tests/functional/n/namePresetCamelCase.txt b/tests/functional/n/namePresetCamelCase.txt index 68bb5aa4bd2..4753fe3c4dd 100644 --- a/tests/functional/n/namePresetCamelCase.txt +++ b/tests/functional/n/namePresetCamelCase.txt @@ -1,3 +1,3 @@ -invalid-name:3::"Constant name ""SOME_CONSTANT"" doesn't conform to camelCase naming style ('([a-z_][A-Za-z0-9]*|__.*__)$' pattern)" -invalid-name:10:MyClass:"Class name ""MyClass"" doesn't conform to camelCase naming style ('[a-z_][a-zA-Z0-9]+$' pattern)" -invalid-name:22:say_hello:"Function name ""say_hello"" doesn't conform to camelCase naming style ('([a-z_][a-zA-Z0-9]{2,}|__[a-z][a-zA-Z0-9_]+__)$' pattern)" +invalid-name:3::"Constant name ""SOME_CONSTANT"" doesn't conform to camelCase naming style ('([^\\W\\dA-Z][^\\W_]*|__.*__)$' pattern)" +invalid-name:10:MyClass:"Class name ""MyClass"" doesn't conform to camelCase naming style ('[^\\W\\dA-Z][^\\W_]+$' pattern)" +invalid-name:22:say_hello:"Function name ""say_hello"" doesn't conform to camelCase naming style ('([^\\W\\dA-Z][^\\W_]{2,}|__[^\\W\\dA-Z_]\\w+__)$' pattern)" diff --git a/tests/functional/n/name_good_bad_names_regex.txt b/tests/functional/n/name_good_bad_names_regex.txt index 5cdef2f4db6..df07c5698db 100644 --- a/tests/functional/n/name_good_bad_names_regex.txt +++ b/tests/functional/n/name_good_bad_names_regex.txt @@ -1,3 +1,3 @@ blacklisted-name:5::"Black listed name ""explicit_bad_some_constant""" -invalid-name:7::"Constant name ""snake_case_bad_SOME_CONSTANT"" doesn't conform to snake_case naming style ('([a-z_][a-z0-9_]*|__.*__)$' pattern)" +invalid-name:7::"Constant name ""snake_case_bad_SOME_CONSTANT"" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]*|__.*__)$' pattern)" blacklisted-name:19:blacklisted_2_snake_case:"Black listed name ""blacklisted_2_snake_case""" diff --git a/tests/functional/n/name_preset_snake_case.txt b/tests/functional/n/name_preset_snake_case.txt index c4c043961dc..91809bdb8e6 100644 --- a/tests/functional/n/name_preset_snake_case.txt +++ b/tests/functional/n/name_preset_snake_case.txt @@ -1,3 +1,3 @@ -invalid-name:3::"Constant name ""SOME_CONSTANT"" doesn't conform to snake_case naming style ('([a-z_][a-z0-9_]*|__.*__)$' pattern)" -invalid-name:10:MyClass:"Class name ""MyClass"" doesn't conform to snake_case naming style ('[a-z_][a-z0-9_]+$' pattern)" -invalid-name:22:sayHello:"Function name ""sayHello"" doesn't conform to snake_case naming style ('([a-z_][a-z0-9_]{2,}|_[a-z0-9_]*|__[a-z][a-z0-9_]+__)$' pattern)" +invalid-name:3::"Constant name ""SOME_CONSTANT"" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]*|__.*__)$' pattern)" +invalid-name:10:MyClass:"Class name ""MyClass"" doesn't conform to snake_case naming style ('[^\\W\\dA-Z][^\\WA-Z]+$' pattern)" +invalid-name:22:sayHello:"Function name ""sayHello"" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]{2,}|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)" diff --git a/tests/functional/n/non_ascii_name.py b/tests/functional/n/non_ascii_name.py new file mode 100644 index 00000000000..276a23f7531 --- /dev/null +++ b/tests/functional/n/non_ascii_name.py @@ -0,0 +1,6 @@ +""" Tests for non-ascii-name checker. """ + +áéíóú = 4444 # [non-ascii-name] + +def úóíéá(): # [non-ascii-name] + """yo""" diff --git a/tests/functional/n/non_ascii_name.txt b/tests/functional/n/non_ascii_name.txt new file mode 100644 index 00000000000..b09b65fadcb --- /dev/null +++ b/tests/functional/n/non_ascii_name.txt @@ -0,0 +1,2 @@ +non-ascii-name:3::"Constant name ""áéíóú"" contains a non-ASCII unicode character" +non-ascii-name:5:úóíéá:"Function name ""úóíéá"" contains a non-ASCII unicode character" diff --git a/tests/test_functional.py b/tests/test_functional.py index 02aa3b82e58..933c85a8382 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -70,6 +70,9 @@ def get_tests(): if dirpath.endswith("__pycache__"): continue for filename in filenames: + if filename == "non_ascii_name.py" and os.name == "nt": + # skip this test on Windows since it involves Unicode + continue if filename != "__init__.py" and filename.endswith(".py"): suite.append(testutils.FunctionalTestFile(dirpath, filename)) return suite @@ -82,7 +85,9 @@ def get_tests(): @pytest.mark.parametrize("test_file", TESTS, ids=TESTS_NAMES) def test_functional(test_file): LintTest = ( - LintModuleOutputUpdate(test_file) if UPDATE else testutils.LintModuleTest(test_file) + LintModuleOutputUpdate(test_file) + if UPDATE + else testutils.LintModuleTest(test_file) ) LintTest.setUp() LintTest._runTest() diff --git a/tests/unittest_checker_base.py b/tests/unittest_checker_base.py index 5d057131e7e..c8832490d8c 100644 --- a/tests/unittest_checker_base.py +++ b/tests/unittest_checker_base.py @@ -443,10 +443,10 @@ def test_comparison(self): class TestNamePresets(unittest.TestCase): - SNAKE_CASE_NAMES = {"test_snake_case", "test_snake_case11", "test_https_200"} - CAMEL_CASE_NAMES = {"testCamelCase", "testCamelCase11", "testHTTP200"} - UPPER_CASE_NAMES = {"TEST_UPPER_CASE", "TEST_UPPER_CASE11", "TEST_HTTP_200"} - PASCAL_CASE_NAMES = {"TestPascalCase", "TestPascalCase11", "TestHTTP200"} + SNAKE_CASE_NAMES = {"tést_snake_case", "test_snake_case11", "test_https_200"} + CAMEL_CASE_NAMES = {"téstCamelCase", "testCamelCase11", "testHTTP200"} + UPPER_CASE_NAMES = {"TÉST_UPPER_CASE", "TEST_UPPER_CASE11", "TEST_HTTP_200"} + PASCAL_CASE_NAMES = {"TéstPascalCase", "TestPascalCase11", "TestHTTP200"} ALL_NAMES = ( SNAKE_CASE_NAMES | CAMEL_CASE_NAMES | UPPER_CASE_NAMES | PASCAL_CASE_NAMES ) From 87681f72477d7bc19e2443203f0e88c10d1ef804 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Wed, 11 Mar 2020 14:21:28 +0100 Subject: [PATCH 068/240] Add exclude_platforms to functional tests and use it for non_ascii_name check --- pylint/checkers/base.py | 2 +- pylint/testutils.py | 8 ++++++++ tests/functional/n/non_ascii_name.rc | 3 +++ tests/test_functional.py | 4 ---- 4 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 tests/functional/n/non_ascii_name.rc diff --git a/pylint/checkers/base.py b/pylint/checkers/base.py index 9b0ead80d12..b7cb6e24579 100644 --- a/pylint/checkers/base.py +++ b/pylint/checkers/base.py @@ -1719,7 +1719,7 @@ class NameChecker(_BasicChecker): "C0144": ( '%s name "%s" contains a non-ASCII unicode character', "non-ascii-name", - "Used when the name contains at least one non-ASCII unciode character.", + "Used when the name contains at least one non-ASCII unicode character.", ), "W0111": ( "Name %s will become a keyword in Python %s", diff --git a/pylint/testutils.py b/pylint/testutils.py index 704ca428607..9b1d98ed5af 100644 --- a/pylint/testutils.py +++ b/pylint/testutils.py @@ -384,6 +384,7 @@ def __init__(self, directory, filename): "max_pyver": (4, 0), "requires": [], "except_implementations": [], + "exclude_platforms": [], } self._parse_options() @@ -530,6 +531,13 @@ def setUp(self): pytest.skip( "Test cannot run with Python implementation %r" % (implementation,) ) + if self._test_file.options["exclude_platforms"]: + platforms = [ + item.strip() + for item in self._test_file.options["exclude_platforms"].split(",") + ] + if sys.platform.lower() in platforms: + pytest.skip("Test cannot run on platform %r" % (sys.platform,)) def _should_be_skipped_due_to_version(self): return ( diff --git a/tests/functional/n/non_ascii_name.rc b/tests/functional/n/non_ascii_name.rc new file mode 100644 index 00000000000..ff09ea1f60b --- /dev/null +++ b/tests/functional/n/non_ascii_name.rc @@ -0,0 +1,3 @@ +[testoptions] +# This test cannot run on Windows due to Unicode error formatting. +exclude_platforms=win32 diff --git a/tests/test_functional.py b/tests/test_functional.py index 933c85a8382..184934e8bfc 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -16,7 +16,6 @@ import csv import io import os -import platform import sys import pytest @@ -70,9 +69,6 @@ def get_tests(): if dirpath.endswith("__pycache__"): continue for filename in filenames: - if filename == "non_ascii_name.py" and os.name == "nt": - # skip this test on Windows since it involves Unicode - continue if filename != "__init__.py" and filename.endswith(".py"): suite.append(testutils.FunctionalTestFile(dirpath, filename)) return suite From 47ec7e7a2937c78136c560b458bc2467bee2acd9 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Wed, 11 Mar 2020 17:11:42 +0100 Subject: [PATCH 069/240] Remove obsolete explicit conversion to integer of a string formatting keyname --- pylint/checkers/utils.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index 9543bf8f163..51299e52fc4 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -575,10 +575,7 @@ def parse_format_method_string( elif name: keyname, fielditerator = split_format_field_names(name) if isinstance(keyname, numbers.Number): - # In Python 2 it will return long which will lead - # to different output between 2 and 3 explicit_pos_args.add(str(keyname)) - keyname = int(keyname) try: keyword_arguments.append((keyname, list(fielditerator))) except ValueError: From a6d7ffc4679b0ad2258cf6c31c6dfbeeb2b331ef Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Wed, 12 Feb 2020 09:18:24 +0100 Subject: [PATCH 070/240] Do not add the current directory to `sys.path` any longer Adding the current directory to `sys.path` can also mean that we're going to load modules having the same name as stdlib modules or astroid modules, which can break pylint. We were doing this since 4becf6f9e596b45401680c4947e2d92c953d5e08, but there was indication on why we were doing that. --- pylint/lint.py | 8 ++++++-- tests/unittest_lint.py | 18 +++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pylint/lint.py b/pylint/lint.py index 578e847ff2b..1960e544b11 100644 --- a/pylint/lint.py +++ b/pylint/lint.py @@ -1475,11 +1475,15 @@ def fix_import_path(args): """ orig = list(sys.path) changes = [] + seen = set() + cwd = os.getcwd() for arg in args: path = _get_python_path(arg) - if path not in changes: + if path not in seen and path != cwd: changes.append(path) - sys.path[:] = changes + ["."] + sys.path + seen.add(path) + + sys.path[:] = changes + sys.path try: yield finally: diff --git a/tests/unittest_lint.py b/tests/unittest_lint.py index 3252a5e4375..336e05e8206 100644 --- a/tests/unittest_lint.py +++ b/tests/unittest_lint.py @@ -165,7 +165,7 @@ def fake_path(): def test_no_args(fake_path): with lint.fix_import_path([]): - assert sys.path == ["."] + fake_path + assert sys.path == fake_path assert sys.path == fake_path @@ -175,7 +175,7 @@ def test_no_args(fake_path): def test_one_arg(fake_path, case): with tempdir() as chroot: create_files(["a/b/__init__.py"]) - expected = [join(chroot, "a")] + ["."] + fake_path + expected = [join(chroot, "a")] + fake_path assert sys.path == fake_path with lint.fix_import_path(case): @@ -195,7 +195,7 @@ def test_one_arg(fake_path, case): def test_two_similar_args(fake_path, case): with tempdir() as chroot: create_files(["a/b/__init__.py", "a/c/__init__.py"]) - expected = [join(chroot, "a")] + ["."] + fake_path + expected = [join(chroot, "a")] + fake_path assert sys.path == fake_path with lint.fix_import_path(case): @@ -214,14 +214,10 @@ def test_two_similar_args(fake_path, case): def test_more_args(fake_path, case): with tempdir() as chroot: create_files(["a/b/c/__init__.py", "a/d/__init__.py", "a/e/f.py"]) - expected = ( - [ - join(chroot, suffix) - for suffix in [sep.join(("a", "b")), "a", sep.join(("a", "e"))] - ] - + ["."] - + fake_path - ) + expected = [ + join(chroot, suffix) + for suffix in [sep.join(("a", "b")), "a", sep.join(("a", "e"))] + ] + fake_path assert sys.path == fake_path with lint.fix_import_path(case): From 7c85b700360653604dd825966624cff359ed4d34 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Fri, 14 Feb 2020 10:55:30 +0100 Subject: [PATCH 071/240] Add test for checking that files are not imported from local directory Close #959 Close #2952 --- tests/test_self.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/test_self.py b/tests/test_self.py index 359d33f5842..65ff6ee4560 100644 --- a/tests/test_self.py +++ b/tests/test_self.py @@ -664,3 +664,35 @@ def test_fail_under(self): ], code=0, ) + + def test_do_not_import_files_from_local_directory(self, tmpdir): + p_astroid = tmpdir / "astroid.py" + p_astroid.write("'Docstring'\nimport completely_unknown\n") + p_hmac = tmpdir / "hmac.py" + p_hmac.write("'Docstring'\nimport completely_unknown\n") + + with tmpdir.as_cwd(): + subprocess.check_output( + [ + sys.executable, + "-m", + "pylint", + "astroid.py", + "--disable=import-error,unused-import", + ], + cwd=str(tmpdir), + ) + + # Test with multiple jobs + with tmpdir.as_cwd(): + subprocess.call( + [ + sys.executable, + "-m", + "pylint", + "-j2", + "hmac.py", + "--disable=import-error,unused-import", + ], + cwd=str(tmpdir), + ) From 3649312233b39a74b4f631bae1e242f5bc7bde2c Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Wed, 11 Mar 2020 15:14:55 +0100 Subject: [PATCH 072/240] Replace individual member imports with `os` in utils file --- pylint/utils/utils.py | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/pylint/utils/utils.py b/pylint/utils/utils.py index 0bf4722c0e2..d5fe8f6ea37 100644 --- a/pylint/utils/utils.py +++ b/pylint/utils/utils.py @@ -4,12 +4,11 @@ # For details: https://github.com/PyCQA/pylint/blob/master/COPYING import codecs +import os import re import sys import textwrap import tokenize -from os import linesep, listdir -from os.path import basename, dirname, exists, isdir, join, normpath, splitext from astroid import Module, modutils @@ -123,18 +122,18 @@ def expand_modules(files_or_modules, black_list, black_list_re): result = [] errors = [] for something in files_or_modules: - if basename(something) in black_list: + if os.path.basename(something) in black_list: continue - if _basename_in_blacklist_re(basename(something), black_list_re): + if _basename_in_blacklist_re(os.path.basename(something), black_list_re): continue - if exists(something): + if os.path.exists(something): # this is a file or a directory try: modname = ".".join(modutils.modpath_from_file(something)) except ImportError: - modname = splitext(basename(something))[0] - if isdir(something): - filepath = join(something, "__init__.py") + modname = os.path.splitext(os.path.basename(something))[0] + if os.path.isdir(something): + filepath = os.path.join(something, "__init__.py") else: filepath = something else: @@ -150,7 +149,7 @@ def expand_modules(files_or_modules, black_list, black_list_re): errors.append({"key": "fatal", "mod": modname, "ex": ex}) continue - filepath = normpath(filepath) + filepath = os.path.normpath(filepath) modparts = (modname or something).split(".") try: @@ -158,7 +157,7 @@ def expand_modules(files_or_modules, black_list, black_list_re): except ImportError: # Might not be acceptable, don't crash. is_namespace = False - is_directory = isdir(something) + is_directory = os.path.isdir(something) else: is_namespace = modutils.is_namespace(spec) is_directory = modutils.is_directory(spec) @@ -176,16 +175,18 @@ def expand_modules(files_or_modules, black_list, black_list_re): has_init = ( not (modname.endswith(".__init__") or modname == "__init__") - and basename(filepath) == "__init__.py" + and os.path.basename(filepath) == "__init__.py" ) if has_init or is_namespace or is_directory: for subfilepath in modutils.get_module_files( - dirname(filepath), black_list, list_all=is_namespace + os.path.dirname(filepath), black_list, list_all=is_namespace ): if filepath == subfilepath: continue - if _basename_in_blacklist_re(basename(subfilepath), black_list_re): + if _basename_in_blacklist_re( + os.path.basename(subfilepath), black_list_re + ): continue modpath = _modpath_from_file(subfilepath, is_namespace) @@ -207,17 +208,19 @@ def register_plugins(linter, directory): 'register' function in each one, used to register pylint checkers """ imported = {} - for filename in listdir(directory): - base, extension = splitext(filename) + for filename in os.listdir(directory): + base, extension = os.path.splitext(filename) if base in imported or base == "__pycache__": continue if ( extension in PY_EXTS and base != "__init__" - or (not extension and isdir(join(directory, base))) + or (not extension and os.path.isdir(os.path.join(directory, base))) ): try: - module = modutils.load_module_from_file(join(directory, filename)) + module = modutils.load_module_from_file( + os.path.join(directory, filename) + ) except ValueError: # empty module name (usually emacs auto-save files) continue @@ -323,7 +326,7 @@ def _check_csv(value): def _comment(string): """return string as a comment""" lines = [line.strip() for line in string.splitlines()] - return "# " + ("%s# " % linesep).join(lines) + return "# " + ("%s# " % os.linesep).join(lines) def _format_option_value(optdict, value): From a96f60eeec49110748d9180724d74a05dd74f136 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Wed, 11 Mar 2020 15:16:18 +0100 Subject: [PATCH 073/240] Move _get_python_path in utils to be accessible by that file as well --- pylint/lint.py | 16 +--------------- pylint/utils/utils.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/pylint/lint.py b/pylint/lint.py index 1960e544b11..5514bec63ca 100644 --- a/pylint/lint.py +++ b/pylint/lint.py @@ -114,20 +114,6 @@ def _get_new_args(message): return (message.msg_id, message.symbol, location, message.msg, message.confidence) -def _get_python_path(filepath): - dirname = os.path.realpath(os.path.expanduser(filepath)) - if not os.path.isdir(dirname): - dirname = os.path.dirname(dirname) - while True: - if not os.path.exists(os.path.join(dirname, "__init__.py")): - return dirname - old_dirname = dirname - dirname = os.path.dirname(dirname) - if old_dirname == dirname: - return os.getcwd() - return None - - def _merge_stats(stats): merged = {} by_msg = collections.Counter() @@ -1478,7 +1464,7 @@ def fix_import_path(args): seen = set() cwd = os.getcwd() for arg in args: - path = _get_python_path(arg) + path = utils.get_python_path(arg) if path not in seen and path != cwd: changes.append(path) seen.add(path) diff --git a/pylint/utils/utils.py b/pylint/utils/utils.py index d5fe8f6ea37..dec15f29a29 100644 --- a/pylint/utils/utils.py +++ b/pylint/utils/utils.py @@ -115,6 +115,20 @@ def _is_package_cb(path, parts): ) +def get_python_path(filepath): + dirname = os.path.realpath(os.path.expanduser(filepath)) + if not os.path.isdir(dirname): + dirname = os.path.dirname(dirname) + while True: + if not os.path.exists(os.path.join(dirname, "__init__.py")): + return dirname + old_dirname = dirname + dirname = os.path.dirname(dirname) + if old_dirname == dirname: + return os.getcwd() + return None + + def expand_modules(files_or_modules, black_list, black_list_re): """take a list of files/modules/packages and return the list of tuple (file, module name) which have to be actually checked From 709e74caca2b2b99863d04328cee94245fb04d8d Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Wed, 11 Mar 2020 15:26:22 +0100 Subject: [PATCH 074/240] Pass an additional search path to modutils.modpath_from_file and friends --- pylint/checkers/imports.py | 1 - pylint/utils/utils.py | 17 ++++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/pylint/checkers/imports.py b/pylint/checkers/imports.py index dea131c61da..49af16ecbac 100644 --- a/pylint/checkers/imports.py +++ b/pylint/checkers/imports.py @@ -776,7 +776,6 @@ def _get_imported_module(self, importnode, modname): except astroid.TooManyLevelsError: if _ignore_import_failure(importnode, modname, self._ignored_modules): return None - self.add_message("relative-beyond-top-level", node=importnode) except astroid.AstroidSyntaxError as exc: message = "Cannot import {!r} due to syntax error {!r}".format( diff --git a/pylint/utils/utils.py b/pylint/utils/utils.py index dec15f29a29..4ec73b9c207 100644 --- a/pylint/utils/utils.py +++ b/pylint/utils/utils.py @@ -135,15 +135,22 @@ def expand_modules(files_or_modules, black_list, black_list_re): """ result = [] errors = [] + path = sys.path.copy() + for something in files_or_modules: if os.path.basename(something) in black_list: continue if _basename_in_blacklist_re(os.path.basename(something), black_list_re): continue + + module_path = get_python_path(something) + additional_search_path = [module_path] + path if os.path.exists(something): # this is a file or a directory try: - modname = ".".join(modutils.modpath_from_file(something)) + modname = ".".join( + modutils.modpath_from_file(something, path=additional_search_path) + ) except ImportError: modname = os.path.splitext(os.path.basename(something))[0] if os.path.isdir(something): @@ -154,7 +161,9 @@ def expand_modules(files_or_modules, black_list, black_list_re): # suppose it's a module or package modname = something try: - filepath = modutils.file_from_modpath(modname.split(".")) + filepath = modutils.file_from_modpath( + modname.split("."), path=additional_search_path + ) if filepath is None: continue except (ImportError, SyntaxError) as ex: @@ -167,7 +176,9 @@ def expand_modules(files_or_modules, black_list, black_list_re): modparts = (modname or something).split(".") try: - spec = modutils.file_info_from_modpath(modparts, path=sys.path) + spec = modutils.file_info_from_modpath( + modparts, path=additional_search_path + ) except ImportError: # Might not be acceptable, don't crash. is_namespace = False From e13aef8c7ead19fe200e1d8e9d273e5a0277278c Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Thu, 12 Mar 2020 09:27:00 +0100 Subject: [PATCH 075/240] Continue adding the current working directory to sys.path with `fix_import_path` This behaviour was previously removed in ac2c49867077cea9d0542560590999f2ebe00276 along with the removal of `''` to not force `pylint` load local files having the same name as stdlib or pylint dependencies. But we need the current working directory in sys.path in order to properly solve relative imports. For instance, given a package `pkg` and two modules, `pkg.A` and `pkg.B`, pylint would not be able to import a relative import `from .B import X` because it does not know where it can look for `.B` in the first place. Having the current working directory in sys.path means that `astroid.modutils.file_info_from_modpath` is still able to solve relative imports. At the same time, having the cwd in sys.path means that we might still load files local to this directory during pylint's initialization. The second part of this commit is to move `fix_import_path` before doing any AST processing. As a side effect, this removes a nasty bug with multiple jobs linting. Because the path was previously modified before spinning up new workers, those new workers would have had the cwd in sys.path and as a result, they would have been "able" to load local files from the directory, such as a malitious `astroid.py`. --- pylint/lint.py | 70 ++++++++++++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/pylint/lint.py b/pylint/lint.py index 5514bec63ca..47f84837831 100644 --- a/pylint/lint.py +++ b/pylint/lint.py @@ -958,15 +958,22 @@ def check(self, files_or_modules): ) filepath = files_or_modules[0] - self._check_files( - functools.partial(self.get_ast, data=_read_stdin()), - [self._get_file_descr_from_stdin(filepath)], - ) + with fix_import_path(files_or_modules): + self._check_files( + functools.partial(self.get_ast, data=_read_stdin()), + [self._get_file_descr_from_stdin(filepath)], + ) elif self.config.jobs == 1: - self._check_files(self.get_ast, self._iterate_file_descrs(files_or_modules)) + with fix_import_path(files_or_modules): + self._check_files( + self.get_ast, self._iterate_file_descrs(files_or_modules) + ) else: check_parallel( - self, self.config.jobs, self._iterate_file_descrs(files_or_modules) + self, + self.config.jobs, + self._iterate_file_descrs(files_or_modules), + files_or_modules, ) def check_single_file(self, name, filepath, modname): @@ -1265,9 +1272,8 @@ def _report_evaluation(self): return note -def check_parallel(linter, jobs, files): - """Use the given linter to lint the files with given amount of workers (jobs) - """ +def check_parallel(linter, jobs, files, arguments=None): + """Use the given linter to lint the files with given amount of workers (jobs)""" # The reporter does not need to be passed to worker processess, i.e. the reporter does # not need to be pickleable original_reporter = linter.reporter @@ -1276,9 +1282,8 @@ def check_parallel(linter, jobs, files): # The linter is inherited by all the pool's workers, i.e. the linter # is identical to the linter object here. This is requred so that # a custom PyLinter object can be used. - with multiprocessing.Pool( - jobs, initializer=_worker_initialize, initargs=[linter] - ) as pool: + initializer = functools.partial(_worker_initialize, arguments=arguments) + with multiprocessing.Pool(jobs, initializer=initializer, initargs=[linter]) as pool: # ..and now when the workers have inherited the linter, the actual reporter # can be set back here on the parent process so that results get stored into # correct reporter @@ -1311,7 +1316,7 @@ def check_parallel(linter, jobs, files): _worker_linter = None -def _worker_initialize(linter): +def _worker_initialize(linter, arguments=None): global _worker_linter # pylint: disable=global-statement _worker_linter = linter @@ -1320,6 +1325,9 @@ def _worker_initialize(linter): _worker_linter.set_reporter(reporters.CollectingReporter()) _worker_linter.open() + # Patch sys.path so that each argument is importable just like in single job mode + _patch_sys_path(arguments or ()) + def _worker_check_single_file(file_item): name, filepath, modname = file_item @@ -1450,30 +1458,35 @@ def preprocess_options(args, search_for): i += 1 -@contextlib.contextmanager -def fix_import_path(args): - """Prepare sys.path for running the linter checks. - - Within this context, each of the given arguments is importable. - Paths are added to sys.path in corresponding order to the arguments. - We avoid adding duplicate directories to sys.path. - `sys.path` is reset to its original value upon exiting this context. - """ - orig = list(sys.path) +def _patch_sys_path(args): + original = list(sys.path) changes = [] seen = set() cwd = os.getcwd() for arg in args: path = utils.get_python_path(arg) - if path not in seen and path != cwd: + if path not in seen: changes.append(path) seen.add(path) sys.path[:] = changes + sys.path + return original + + +@contextlib.contextmanager +def fix_import_path(args): + """Prepare sys.path for running the linter checks. + + Within this context, each of the given arguments is importable. + Paths are added to sys.path in corresponding order to the arguments. + We avoid adding duplicate directories to sys.path. + `sys.path` is reset to its original value upon exiting this context. + """ + original = _patch_sys_path(args) try: yield finally: - sys.path[:] = orig + sys.path[:] = original class Run: @@ -1754,11 +1767,8 @@ def __init__(self, args, reporter=None, do_exit=True): # load plugin specific configuration. linter.load_plugin_configuration() - # insert current working directory to the python path to have a correct - # behaviour - with fix_import_path(args): - linter.check(args) - score_value = linter.generate_reports() + linter.check(args) + score_value = linter.generate_reports() if do_exit: if linter.config.exit_zero: sys.exit(0) From 5d63f2a13c7aea499ae648fb00d86b43f24a9ba2 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Thu, 12 Mar 2020 10:22:46 +0100 Subject: [PATCH 076/240] Add more tests for loading a malitious astroid file --- pylint/lint.py | 1 - tests/test_self.py | 17 ++++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/pylint/lint.py b/pylint/lint.py index 47f84837831..acee9c7fa53 100644 --- a/pylint/lint.py +++ b/pylint/lint.py @@ -1462,7 +1462,6 @@ def _patch_sys_path(args): original = list(sys.path) changes = [] seen = set() - cwd = os.getcwd() for arg in args: path = utils.get_python_path(arg) if path not in seen: diff --git a/tests/test_self.py b/tests/test_self.py index 65ff6ee4560..38b4ed4a490 100644 --- a/tests/test_self.py +++ b/tests/test_self.py @@ -683,7 +683,22 @@ def test_do_not_import_files_from_local_directory(self, tmpdir): cwd=str(tmpdir), ) - # Test with multiple jobs + # Linting this astroid file does not import it + with tmpdir.as_cwd(): + subprocess.check_output( + [ + sys.executable, + "-m", + "pylint", + "-j2", + "astroid.py", + "--disable=import-error,unused-import", + ], + cwd=str(tmpdir), + ) + + # Test with multiple jobs for hmac.py for which we have a + # CVE against: https://github.com/PyCQA/pylint/issues/959 with tmpdir.as_cwd(): subprocess.call( [ From c9ed11201ff89b93dc8f6cc1bcd2f723c5cfc965 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Fri, 13 Mar 2020 11:08:59 +0100 Subject: [PATCH 077/240] Add regression test for unused-argument and raise Close #3416 --- pylint/testutils.py | 5 +++-- .../r/regression_3416_unused_argument_raise.py | 12 ++++++++++++ .../r/regression_3416_unused_argument_raise.txt | 3 +++ 3 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 tests/functional/r/regression_3416_unused_argument_raise.py create mode 100644 tests/functional/r/regression_3416_unused_argument_raise.txt diff --git a/pylint/testutils.py b/pylint/testutils.py index 9b1d98ed5af..91deee01e9f 100644 --- a/pylint/testutils.py +++ b/pylint/testutils.py @@ -617,6 +617,7 @@ def _split_lines(cls, expected_messages, lines): return emitted, omitted def _check_output_text(self, expected_messages, expected_lines, received_lines): + expected_lines = self._split_lines(expected_messages, expected_lines)[0] assert ( - self._split_lines(expected_messages, expected_lines)[0] == received_lines - ), "Error with the following functional test: {}".format(self._test_file.base) + expected_lines == received_lines + ), "Expected test lines did not match for test: {}".format(self._test_file.base) diff --git a/tests/functional/r/regression_3416_unused_argument_raise.py b/tests/functional/r/regression_3416_unused_argument_raise.py new file mode 100644 index 00000000000..2d508728830 --- /dev/null +++ b/tests/functional/r/regression_3416_unused_argument_raise.py @@ -0,0 +1,12 @@ +"""Test that we emit unused-argument when a function uses `raise + +https://github.com/PyCQA/pylint/issues/3416 +""" + +# +1: [unused-argument, unused-argument, unused-argument] +def fun(arg_a, arg_b, arg_c) -> None: + """Routine docstring""" + try: + pass + except Exception: + raise RuntimeError("") diff --git a/tests/functional/r/regression_3416_unused_argument_raise.txt b/tests/functional/r/regression_3416_unused_argument_raise.txt new file mode 100644 index 00000000000..ecfd97fbefe --- /dev/null +++ b/tests/functional/r/regression_3416_unused_argument_raise.txt @@ -0,0 +1,3 @@ +unused-argument:7:fun:Unused argument 'arg_a' +unused-argument:7:fun:Unused argument 'arg_b' +unused-argument:7:fun:Unused argument 'arg_c' From 9d25804b91205f9815ea5b3dd767eb4a0329cbce Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Fri, 13 Mar 2020 12:05:02 +0100 Subject: [PATCH 078/240] Do not use pkginfo.modname any longer --- pylint/config.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pylint/config.py b/pylint/config.py index 419a5a93477..5b09a74f67d 100644 --- a/pylint/config.py +++ b/pylint/config.py @@ -48,7 +48,6 @@ from typing import Any, Dict, Tuple import toml - from pylint import utils USER_HOME = os.path.expanduser("~") @@ -512,7 +511,7 @@ def format_tail(pkginfo): .SH AUTHOR %s <%s> """ % ( - getattr(pkginfo, "debian_name", pkginfo.modname), + getattr(pkginfo, "debian_name", "pylint"), pkginfo.mailinglist, pkginfo.author, pkginfo.author_email, From bd85d3b70d76b94bfb5b104bfe46daab965c51af Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Fri, 13 Mar 2020 12:20:02 +0100 Subject: [PATCH 079/240] Refresh the docs a bit in preparation for the release --- ChangeLog | 6 +- doc/whatsnew/2.5.rst | 76 ++++++------- examples/pylintrc | 140 +++++++++++++----------- man/pylint.1 | 247 ++++++++++++++++++++++--------------------- 4 files changed, 243 insertions(+), 226 deletions(-) diff --git a/ChangeLog b/ChangeLog index 81750fae009..0b5ad6f2b0f 100644 --- a/ChangeLog +++ b/ChangeLog @@ -41,7 +41,7 @@ Release date: TBA * ``function-redefined`` exempts function redefined on a condition. - Close #2410 + Close #2410 * ``typing.overload`` functions are exempted from docstring checks @@ -98,11 +98,11 @@ Release date: TBA * ``missing-*-docstring`` can look for ``__doc__`` assignments. - Close #3301 + Close #3301 * ``undefined-variable`` can now find undefined loop iterables - Close #498 + Close #498 * ``safe_infer`` can infer a value as long as all the paths share the same type. diff --git a/doc/whatsnew/2.5.rst b/doc/whatsnew/2.5.rst index 3fb98236d1f..aa48b615f48 100644 --- a/doc/whatsnew/2.5.rst +++ b/doc/whatsnew/2.5.rst @@ -13,23 +13,23 @@ Summary -- Release highlights New checkers ============ -* A new check ``isinstance-second-argument-not-valid-type``` was added. +* A new check ``isinstance-second-argument-not-valid-type`` was added. - This check is emitted whenever **pylint** finds a call to the `isinstance` - function with a second argument that is not a type. Such code is likely - unintended as it will cause a TypeError to be thrown at runtime error. + This check is emitted whenever **pylint** finds a call to the `isinstance` + function with a second argument that is not a type. Such code is likely + unintended as it will cause a TypeError to be thrown at runtime error. * A new check ``assert-on-string-literal`` was added. - This check is emitted whenever **pylint** finds an assert statement - with a string literal as its first argument. Such assert statements - are probably unintended as they will always pass. + This check is emitted whenever **pylint** finds an assert statement + with a string literal as its first argument. Such assert statements + are probably unintended as they will always pass. * A new check ``f-string-without-interpolation`` was added. - This check is emitted whenever **pylint** detects the use of an - f-string without having any interpolated values in it, which means - that the f-string can be a normal string. + This check is emitted whenever **pylint** detects the use of an + f-string without having any interpolated values in it, which means + that the f-string can be a normal string. * Multiple checks for invalid return types of protocol functions were added: @@ -46,51 +46,45 @@ New checkers * A new check ``inconsistent-quotes`` was added. - This check is emitted when quotes delimiters (" and ') are not used - consistently throughout a module. It makes allowances for avoiding - unnecessary escaping, allowing, for example, ``"Don't error"`` in a module in - which single-quotes otherwise delimit strings so that the single quote in - ``Don't`` doesn't need to be escaped. + This check is emitted when quotes delimiters (``"`` and ``'``) are not used + consistently throughout a module. It allows avoiding unnecessary escaping, + allowing, for example, ``"Don't error"`` in a module in which single-quotes + otherwise delimit strings so that the single quote in ``Don't`` doesn't need to be escaped. + +* A new check ``non-str-assignment-to-dunder-name`` was added to ensure that only strings are assigned to ``__name__`` attributes. Other Changes ============= -* Don't emit ``line-too-long`` for multilines when a - `pylint:disable=line-too-long` comment stands at their end. +* Configuration can be read from a setup.cfg or pyproject.toml file in the current directory. + A setup.cfg must prepend pylintrc section names with ``pylint.``, for example ``[pylint.MESSAGES CONTROL]``. + A pyproject.toml file must prepend section names with ``tool.pylint.``, for example ``[tool.pylint.'MESSAGES CONTROL']``. + These files can also be passed in on the command line. - For example the following code will not trigger any ``line-too-long`` message:: +* Add new ``good-names-rgx`` and ``bad-names-rgx`` to enable permitting or disallowing of names via regular expressions - def example(): - """ - This is a very very very long line within a docstring that should trigger a pylint C0301 error line-too-long + To enable better handling of whitelisting/blacklisting names, we added two new config options: good-names-rgxs: a comma- + separated list of regexes, that if a name matches will be exempt of naming-checking. bad-names-rgxs: a comma- + separated list of regexes, that if a name matches will be always marked as a blacklisted name. - Even spread on multiple lines, the disable command is still effective on very very very, maybe too much long docstring - """#pylint: disable=line-too-long - pass +* Mutable ``collections.*`` are now flagged as dangerous defaults. -* Configuration can be read from a setup.cfg or pyproject.toml file - in the current directory. - A setup.cfg must prepend pylintrc section names with ``pylint.``, - for example ``[pylint.MESSAGES CONTROL]``. - A pyproject.toml file must prepend section names with ``tool.pylint.``, - for example ``[tool.pylint.'MESSAGES CONTROL']``. - These files can also be passed in on the command line. +* Add new ``--fail-under`` flag for setting the threshold for the score to fail overall tests. If the score is over the fail-under threshold, pylint will complete SystemExit with value 0 to indicate no errors. -* Add new good-names-rgx and bad-names-rgx to enable white-/blacklisting of regular expressions +* Added a new option ``notes-rgx`` to make fixme warnings more flexible. Now either ``notes`` or ``notes-rgx`` option can be used to detect fixme warnings. -To enable better handling of whitelisting/blacklisting names, we added two new config options: good-names-rgxs: a comma- -separated list of regexes, that if a name matches will be exempt of naming-checking. bad-names-rgxs: a comma- -separated list of regexes, that if a name matches will be always marked as a blacklisted name. +* Non-ASCII characters are now allowed by ``invalid-name``. -* Mutable ``collections.*`` are now flagged as dangerous defaults. +* ``pylint`` no longer emits ``invalid-name`` for non-constants found at module level. -* Add new --fail-under flag for setting the threshold for the score to fail overall tests. If the score is over the fail-under threshold, pylint will complete SystemExit with value 0 to indicate no errors. + Pylint was considering all module level variables as constants, which is not what PEP 8 is actually mandating. -* Add a new check (non-str-assignment-to-dunder-name) to ensure that only strings are assigned to ``__name__`` attributes +* A new check ``non-ascii-name`` was added to detect identifiers with non-ASCII characters. -* Add a new option ``notes-rgx`` to make fixme warnings more flexible. Now either ``notes`` or ``notes-rgx`` option can be used to detect fixme warnings. +* Overloaded typing functions no longer trigger ``no-self-use``, ``unused-argument``, ``missing-docstring`` and similar checks + that assumed that overloaded functions are normal functions. -* Non-ASCII characters are now allowed by ``invalid-name``. +* ``python -m pylint`` can no longer be made to import files from the local directory. -* Add a new check ``non-ascii-name`` to detect identifiers with non-ASCII characters. +* Various false positives have been fixed which you can read more about in the Changelog files. diff --git a/examples/pylintrc b/examples/pylintrc index 7196a820343..f6aad454ef8 100644 --- a/examples/pylintrc +++ b/examples/pylintrc @@ -5,6 +5,9 @@ # run arbitrary code. extension-pkg-whitelist= +# Specify a score threshold to be exceeded before program exits with error. +fail-under=10 + # Add files or directories to the blacklist. They should be base names, not # paths. ignore=CVS @@ -33,9 +36,6 @@ load-plugins= # Pickle collected data for later comparisons. persistent=yes -# Specify a configuration file. -#rcfile= - # When enabled, pylint would attempt to guess common misconfiguration and emit # user-friendly hints instead of false-positive error messages. suggestion-mode=yes @@ -184,6 +184,48 @@ max-nested-blocks=5 never-returning-functions=sys.exit +[LOGGING] + +# Format style used to check logging format string. `old` means using % +# formatting, `new` is for `{}` formatting,and `fstr` is for f-strings. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it work, +# install the python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +#notes-rgx= + + [TYPECHECK] # List of decorators that produce context managers, such as @@ -239,32 +281,6 @@ missing-member-max-choices=1 signature-mutators= -[LOGGING] - -# Format style used to check logging format string. `old` means using % -# formatting, `new` is for `{}` formatting, and `fstr` is for f-strings. -logging-format-style=old - -# Logging modules to check that the string format arguments are in logging -# function parameter format. -logging-modules=logging - - -[SIMILARITIES] - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - -# Minimum lines number of a similarity. -min-similarity-lines=4 - - [VARIABLES] # List of additional names supposed to be defined in builtins. Remember that @@ -295,26 +311,6 @@ init-import=no redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io -[SPELLING] - -# Limits count of emitted suggestions for spelling mistakes. -max-spelling-suggestions=4 - -# Spelling dictionary name. Available dictionaries: none. To make it work, -# install the python-enchant package. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains the private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to the private dictionary (see the -# --spelling-private-dict-file option) instead of raising a message. -spelling-store-unknown-words=no - - [FORMAT] # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. @@ -352,6 +348,21 @@ single-line-class-stmt=no single-line-if-stmt=no +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + [BASIC] # Naming style matching correct argument names. @@ -376,6 +387,10 @@ bad-names=foo, tutu, tata +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + # Naming style matching correct class attribute names. class-attribute-naming-style=any @@ -416,6 +431,10 @@ good-names=i, Run, _ +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + # Include a hint for the correct naming format with invalid-name. include-naming-hint=no @@ -461,30 +480,23 @@ variable-naming-style=snake_case #variable-rgx= -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME, - XXX, - TODO - -# Regular expressio of note tags to take in consideration. -notes-rgx=a^ - [STRING] -# This flag controls whether the implicit-str-concat-in-sequence should -# generate a warning on implicit string concatenation in sequences defined over -# several lines. -check-str-concat-over-line-jumps=no - # This flag controls whether inconsistent-quotes generates a warning when the # character used as a quote delimiter is used inconsistently within a module. check-quote-consistency=no +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + [IMPORTS] +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + # Allow wildcard imports from modules that define __all__. allow-wildcard-with-all=no diff --git a/man/pylint.1 b/man/pylint.1 index 56d5ec3e81c..7e0e4e94c2f 100644 --- a/man/pylint.1 +++ b/man/pylint.1 @@ -1,4 +1,4 @@ -.TH pylint 1 "2019-06-29" pylint +.TH pylint 1 "2020-03-13" pylint .SH NAME .B pylint \- python code static checker @@ -38,8 +38,6 @@ show this help message and exit more verbose help. .SH MASTER -.IP "--rcfile=" -Specify a configuration file. .IP "--init-hook=" Python code to execute, usually for sys.path manipulation such as pygtk.require(). .IP "--errors-only, -E" @@ -56,6 +54,8 @@ Add files or directories matching the regex patterns to the blacklist. The regex Pickle collected data for later comparisons. [default: yes] .IP "--load-plugins=" List of plugins (as comma separated values of python module names) to load, usually to register additional checkers. [default: none] +.IP "--fail-under=" +Specify a score threshold to be exceeded before program exits with error. [default: 10] .IP "--jobs=, -j " Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the number of processors available to use. [default: 1] .IP "--limit-inference-results=" @@ -70,10 +70,14 @@ Always return a 0 (non-error) status code, even if lint errors are found. This i Interpret the stdin as a python script, whose filename needs to be passed as the module_or_package argument. .SH COMMANDS +.IP "--rcfile=" +Specify a configuration file to load. .IP "--help-msg=" Display a help message for the given message id and exit. The value may be a comma separated list of message ids. .IP "--list-msgs" Generate pylint's messages. +.IP "--list-msgs-enabled" +Display a list of what messages are enabled and disabled with the given configuration. .IP "--list-groups" List pylint's message groups. .IP "--list-conf-levels" @@ -103,6 +107,30 @@ Activate the evaluation score. [default: yes] .IP "--msg-template=