diff --git a/doc/tutorial.rst b/doc/tutorial.rst index fce4722341..9dee73e1e7 100644 --- a/doc/tutorial.rst +++ b/doc/tutorial.rst @@ -64,10 +64,12 @@ preferences a bit better. Your First Pylint'ing --------------------- -We'll use a basic Python script with ``black`` already applied on it, +We'll use a basic Python script with `black`_ already applied on it, as fodder for our tutorial. The starting code we will use is called ``simplecaesar.py`` and is here in its entirety: +.. _`black`: https://github.com/psf/black + .. sourcecode:: python #!/usr/bin/env python3 diff --git a/doc/user_guide/configuration/all-options.rst b/doc/user_guide/configuration/all-options.rst index e74c967a1a..6c1afb029c 100644 --- a/doc/user_guide/configuration/all-options.rst +++ b/doc/user_guide/configuration/all-options.rst @@ -1492,7 +1492,7 @@ Standard Checkers --missing-member-hint-distance """""""""""""""""""""""""""""" -*The minimum edit distance a name should have in order to be considered a similar match for a missing member name.* +*The maximum edit distance a name should have in order to be considered a similar match for a missing member name.* **Default:** ``1`` diff --git a/doc/whatsnew/3/3.3/index.rst b/doc/whatsnew/3/3.3/index.rst index b6bb419f4b..29c4834c0a 100644 --- a/doc/whatsnew/3/3.3/index.rst +++ b/doc/whatsnew/3/3.3/index.rst @@ -14,6 +14,47 @@ Summary -- Release highlights .. towncrier release notes start +What's new in Pylint 3.3.7? +--------------------------- +Release date: 2025-05-04 + + +False Positives Fixed +--------------------- + +- Comparisons between two calls to `type()` won't raise an ``unidiomatic-typecheck`` warning anymore, consistent with the behavior applied only for ``==`` previously. + + Closes #10161 (`#10161 `_) + + + +Other Bug Fixes +--------------- + +- Fixed a crash when importing a class decorator that did not exist with the same name as a class attribute after the class definition. + + Closes #10105 (`#10105 `_) + +- Fix a crash caused by malformed format strings when using `.format` with keyword arguments. + + Closes #10282 (`#10282 `_) + +- Using a slice as a class decorator now raises a ``not-callable`` message instead of crashing. A lot of checks that dealt with decorators (too many to list) are now shortcut if the decorator can't immediately be inferred to a function or class definition. + + Closes #10334 (`#10334 `_) + + + +Other Changes +------------- + +- The algorithm used for ``no-member`` suggestions is now more efficient and cuts the + calculation when the distance score is already above the threshold. + + Refs #10277 (`#10277 `_) + + + What's new in Pylint 3.3.6? --------------------------- Release date: 2025-03-20 diff --git a/examples/pylintrc b/examples/pylintrc index 97064b3e95..8c7580fd0c 100644 --- a/examples/pylintrc +++ b/examples/pylintrc @@ -495,10 +495,10 @@ evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor # used to format the message information. See doc for all details. msg-template= -# Set the output format. Available formats are: text, parseable, colorized, -# json2 (improved json format), json (old json format) and msvs (visual -# studio). You can also give a reporter class, e.g. -# mypackage.mymodule.MyReporterClass. +# Set the output format. Available formats are: 'text', 'parseable', +# 'colorized', 'json2' (improved json format), 'json' (old json format), msvs +# (visual studio) and 'github' (GitHub actions). You can also give a reporter +# class, e.g. mypackage.mymodule.MyReporterClass. #output-format= # Tells whether to display a full report or only the messages. @@ -600,7 +600,7 @@ ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace # of finding the hint is based on edit distance. missing-member-hint=yes -# The minimum edit distance a name should have in order to be considered a +# The maximum edit distance a name should have in order to be considered a # similar match for a missing member name. missing-member-hint-distance=1 diff --git a/examples/pyproject.toml b/examples/pyproject.toml index d37e9d427e..33df0cafc5 100644 --- a/examples/pyproject.toml +++ b/examples/pyproject.toml @@ -438,9 +438,10 @@ evaluation = "max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refact # used to format the message information. See doc for all details. # msg-template = -# Set the output format. Available formats are: text, parseable, colorized, json2 -# (improved json format), json (old json format) and msvs (visual studio). You -# can also give a reporter class, e.g. mypackage.mymodule.MyReporterClass. +# Set the output format. Available formats are: 'text', 'parseable', 'colorized', +# 'json2' (improved json format), 'json' (old json format), msvs (visual studio) +# and 'github' (GitHub actions). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. # output-format = # Tells whether to display a full report or only the messages. @@ -529,7 +530,7 @@ ignored-classes = [ "optparse.Values", "thread._local", "_thread._local", "argpa # finding the hint is based on edit distance. missing-member-hint = true -# The minimum edit distance a name should have in order to be considered a +# The maximum edit distance a name should have in order to be considered a # similar match for a missing member name. missing-member-hint-distance = 1 diff --git a/pylint/__pkginfo__.py b/pylint/__pkginfo__.py index a6f117f4c9..6d9fccb5bd 100644 --- a/pylint/__pkginfo__.py +++ b/pylint/__pkginfo__.py @@ -9,7 +9,7 @@ from __future__ import annotations -__version__ = "3.3.6" +__version__ = "3.3.7" def get_numversion_from_version(v: str) -> tuple[int, int, int]: diff --git a/pylint/checkers/base/comparison_checker.py b/pylint/checkers/base/comparison_checker.py index 6fb053e2e1..091c8e9db3 100644 --- a/pylint/checkers/base/comparison_checker.py +++ b/pylint/checkers/base/comparison_checker.py @@ -323,14 +323,10 @@ def _check_unidiomatic_typecheck(self, node: nodes.Compare) -> None: if operator in TYPECHECK_COMPARISON_OPERATORS: left = node.left if _is_one_arg_pos_call(left): - self._check_type_x_is_y(node, left, operator, right) + self._check_type_x_is_y(node=node, left=left, right=right) def _check_type_x_is_y( - self, - node: nodes.Compare, - left: nodes.NodeNG, - operator: str, - right: nodes.NodeNG, + self, node: nodes.Compare, left: nodes.NodeNG, right: nodes.NodeNG ) -> None: """Check for expressions like type(x) == Y.""" left_func = utils.safe_infer(left.func) @@ -339,7 +335,7 @@ def _check_type_x_is_y( ): return - if operator in {"is", "is not"} and _is_one_arg_pos_call(right): + if _is_one_arg_pos_call(right): right_func = utils.safe_infer(right.func) if ( isinstance(right_func, nodes.ClassDef) diff --git a/pylint/checkers/deprecated.py b/pylint/checkers/deprecated.py index 028dc13f38..a3e0e85ebf 100644 --- a/pylint/checkers/deprecated.py +++ b/pylint/checkers/deprecated.py @@ -136,7 +136,9 @@ def visit_decorators(self, node: nodes.Decorators) -> None: inf = safe_infer(children[0].func) else: inf = safe_infer(children[0]) - qname = inf.qname() if inf else None + if not isinstance(inf, (nodes.ClassDef, nodes.FunctionDef)): + return + qname = inf.qname() if qname in self.deprecated_decorators(): self.add_message("deprecated-decorator", node=node, args=qname) diff --git a/pylint/checkers/refactoring/recommendation_checker.py b/pylint/checkers/refactoring/recommendation_checker.py index c5b19e1a55..8824455750 100644 --- a/pylint/checkers/refactoring/recommendation_checker.py +++ b/pylint/checkers/refactoring/recommendation_checker.py @@ -382,6 +382,14 @@ def _detect_replacable_format_call(self, node: nodes.Const) -> None: if not isinstance(node.parent.parent, nodes.Call): return + # Don't raise message on bad format string + try: + keyword_args = [ + i[0] for i in utils.parse_format_method_string(node.value)[0] + ] + except utils.IncompleteFormatString: + return + if node.parent.parent.args: for arg in node.parent.parent.args: # If star expressions with more than 1 element are being used @@ -397,9 +405,6 @@ def _detect_replacable_format_call(self, node: nodes.Const) -> None: return elif node.parent.parent.keywords: - keyword_args = [ - i[0] for i in utils.parse_format_method_string(node.value)[0] - ] for keyword in node.parent.parent.keywords: # If keyword is used multiple times if keyword_args.count(keyword.arg) > 1: @@ -408,9 +413,12 @@ def _detect_replacable_format_call(self, node: nodes.Const) -> None: keyword = utils.safe_infer(keyword.value) # If lists of more than one element are being unpacked - if isinstance(keyword, nodes.Dict): - if len(keyword.items) > 1 and len(keyword_args) > 1: - return + if ( + isinstance(keyword, nodes.Dict) + and len(keyword.items) > 1 + and len(keyword_args) > 1 + ): + return # If all tests pass, then raise message self.add_message( @@ -438,12 +446,10 @@ def _detect_replacable_format_call(self, node: nodes.Const) -> None: inferred_right = utils.safe_infer(node.parent.right) # If dicts or lists of length > 1 are used - if isinstance(inferred_right, nodes.Dict): - if len(inferred_right.items) > 1: - return - elif isinstance(inferred_right, nodes.List): - if len(inferred_right.elts) > 1: - return + if isinstance(inferred_right, nodes.Dict) and len(inferred_right.items) > 1: + return + if isinstance(inferred_right, nodes.List) and len(inferred_right.elts) > 1: + return # If all tests pass, then raise message self.add_message( diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index bc7ddfc2a4..1d8fc4364e 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -150,8 +150,12 @@ def _(node: nodes.ClassDef | bases.Instance) -> Iterable[str]: return itertools.chain(values, other_values) -def _string_distance(seq1: str, seq2: str) -> int: - seq2_length = len(seq2) +def _string_distance(seq1: str, seq2: str, seq1_length: int, seq2_length: int) -> int: + if not seq1_length: + return seq2_length + + if not seq2_length: + return seq1_length row = [*list(range(1, seq2_length + 1)), 0] for seq1_index, seq1_char in enumerate(seq1): @@ -182,11 +186,20 @@ def _similar_names( possible_names: list[tuple[str, int]] = [] names = _node_names(owner) + attr_str = attrname or "" + attr_len = len(attr_str) + for name in names: if name == attrname: continue - distance = _string_distance(attrname or "", name) + name_len = len(name) + + min_distance = abs(attr_len - name_len) + if min_distance > distance_threshold: + continue + + distance = _string_distance(attr_str, name, attr_len, name_len) if distance <= distance_threshold: possible_names.append((name, distance)) @@ -947,7 +960,7 @@ class TypeChecker(BaseChecker): "default": 1, "type": "int", "metavar": "", - "help": "The minimum edit distance a name should have in order " + "help": "The maximum edit distance a name should have in order " "to be considered a similar match for a missing member name.", }, ), diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index bfc4bc61da..374ae31e80 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -876,7 +876,7 @@ def decorated_with( if any( i.name in qnames or i.qname() in qnames for i in decorator_node.infer() - if i is not None and not isinstance(i, util.UninferableBase) + if isinstance(i, (nodes.ClassDef, nodes.FunctionDef)) ): return True except astroid.InferenceError: diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index f478e16d3b..502ef481b9 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -2448,7 +2448,7 @@ def _is_only_type_assignment( node_frame = node.frame() parent = node - while parent is not defstmt_frame.parent: + while parent not in {defstmt_frame.parent, None}: parent_scope = parent.scope() # Find out if any nonlocals receive values in nested functions diff --git a/pylintrc b/pylintrc index bd6e8a2e22..78f9e16629 100644 --- a/pylintrc +++ b/pylintrc @@ -378,7 +378,7 @@ ignore-on-opaque-inference=yes # of finding the hint is based on edit distance. missing-member-hint=yes -# The minimum edit distance a name should have in order to be considered a +# The maximum edit distance a name should have in order to be considered a # similar match for a missing member name. missing-member-hint-distance=1 diff --git a/pyproject.toml b/pyproject.toml index 0cf1ade578..a3cc3f43b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [build-system] build-backend = "setuptools.build_meta" -requires = [ "setuptools>=71.0.4" ] +requires = [ "setuptools>=77" ] [project] name = "pylint" diff --git a/tbump.toml b/tbump.toml index c83b9d67b5..5da7f7ef37 100644 --- a/tbump.toml +++ b/tbump.toml @@ -1,7 +1,7 @@ github_url = "https://github.com/pylint-dev/pylint" [version] -current = "3.3.6" +current = "3.3.7" regex = ''' ^(?P0|[1-9]\d*) \. diff --git a/tests/checkers/unittest_typecheck.py b/tests/checkers/unittest_typecheck.py index c944b863f3..d3fd5a34c0 100644 --- a/tests/checkers/unittest_typecheck.py +++ b/tests/checkers/unittest_typecheck.py @@ -221,3 +221,47 @@ def decorated(): ) ): self.checker.visit_subscript(subscript) + + +class TestTypeCheckerStringDistance: + """Tests for the _string_distance helper in pylint.checkers.typecheck.""" + + def test_string_distance_identical_strings(self) -> None: + seq1 = "hi" + seq2 = "hi" + assert typecheck._string_distance(seq1, seq2, len(seq1), len(seq2)) == 0 + + seq1, seq2 = seq2, seq1 + assert typecheck._string_distance(seq1, seq2, len(seq1), len(seq2)) == 0 + + def test_string_distance_empty_string(self) -> None: + seq1 = "" + seq2 = "hi" + assert typecheck._string_distance(seq1, seq2, len(seq1), len(seq2)) == 2 + + seq1, seq2 = seq2, seq1 + assert typecheck._string_distance(seq1, seq2, len(seq1), len(seq2)) == 2 + + def test_string_distance_edit_distance_one_character(self) -> None: + seq1 = "hi" + seq2 = "he" + assert typecheck._string_distance(seq1, seq2, len(seq1), len(seq2)) == 1 + + seq1, seq2 = seq2, seq1 + assert typecheck._string_distance(seq1, seq2, len(seq1), len(seq2)) == 1 + + def test_string_distance_edit_distance_multiple_similar_characters(self) -> None: + seq1 = "hello" + seq2 = "yelps" + assert typecheck._string_distance(seq1, seq2, len(seq1), len(seq2)) == 3 + + seq1, seq2 = seq2, seq1 + assert typecheck._string_distance(seq1, seq2, len(seq1), len(seq2)) == 3 + + def test_string_distance_edit_distance_all_dissimilar_characters(self) -> None: + seq1 = "yellow" + seq2 = "orange" + assert typecheck._string_distance(seq1, seq2, len(seq1), len(seq2)) == 6 + + seq1, seq2 = seq2, seq1 + assert typecheck._string_distance(seq1, seq2, len(seq1), len(seq2)) == 6 diff --git a/tests/functional/c/consider/consider_using_f_string.py b/tests/functional/c/consider/consider_using_f_string.py index 086fb3f875..d40b417400 100644 --- a/tests/functional/c/consider/consider_using_f_string.py +++ b/tests/functional/c/consider/consider_using_f_string.py @@ -128,3 +128,11 @@ def wrap_print(value): print(value) wrap_print(value="{}".format) + + +def invalid_format_string_good(): + """Should not raise message when `.format` is called with an invalid format string.""" + # pylint: disable=bad-format-string + print("{a[0] + a[1]}".format(a=[0, 1])) + print("{".format(a=1)) + print("{".format(1)) diff --git a/tests/functional/r/regression_02/regression_10105.py b/tests/functional/r/regression_02/regression_10105.py new file mode 100644 index 0000000000..9eeaeac1b6 --- /dev/null +++ b/tests/functional/r/regression_02/regression_10105.py @@ -0,0 +1,9 @@ +# pylint: disable=too-few-public-methods,missing-docstring,used-before-assignment,import-error,unused-import +# pylint: disable=wrong-import-position + + +@decorator +class DecoratedClass: + decorator: int + +import decorator diff --git a/tests/functional/r/regression_02/regression_10334.py b/tests/functional/r/regression_02/regression_10334.py new file mode 100644 index 0000000000..772860846e --- /dev/null +++ b/tests/functional/r/regression_02/regression_10334.py @@ -0,0 +1,6 @@ +"""Test for slice object used as a decorator.""" +# pylint: disable=too-few-public-methods +s = slice(-2) +@s() # [not-callable] +class A: + """Class with a slice decorator.""" diff --git a/tests/functional/r/regression_02/regression_10334.txt b/tests/functional/r/regression_02/regression_10334.txt new file mode 100644 index 0000000000..d2baba926e --- /dev/null +++ b/tests/functional/r/regression_02/regression_10334.txt @@ -0,0 +1 @@ +not-callable:4:1:4:4:A:s is not callable:UNDEFINED diff --git a/tests/functional/u/unidiomatic_typecheck.py b/tests/functional/u/unidiomatic_typecheck.py index 2a1957d75e..5e96a2745a 100644 --- a/tests/functional/u/unidiomatic_typecheck.py +++ b/tests/functional/u/unidiomatic_typecheck.py @@ -57,14 +57,28 @@ def parameter_shadowing_inference_negatives(type): type(42) in [int] type(42) not in [int] -def deliberate_subclass_check_negatives(b): + +def deliberate_subclass_check_negatives(a, b): type(42) is type(b) type(42) is not type(b) + type(42) == type(b) + type(42) != type(b) + type(a) is type(b) + type(a) is not type(b) + type(a) == type(b) + type(a) != type(b) + def type_of_literals_positives(a): - type(a) is type([]) # [unidiomatic-typecheck] - type(a) is not type([]) # [unidiomatic-typecheck] - type(a) is type({}) # [unidiomatic-typecheck] - type(a) is not type({}) # [unidiomatic-typecheck] - type(a) is type("") # [unidiomatic-typecheck] - type(a) is not type("") # [unidiomatic-typecheck] + type(a) is type([]) # [unidiomatic-typecheck] + type(a) is not type([]) # [unidiomatic-typecheck] + type(a) is type({}) # [unidiomatic-typecheck] + type(a) is not type({}) # [unidiomatic-typecheck] + type(a) is type("") # [unidiomatic-typecheck] + type(a) is not type("") # [unidiomatic-typecheck] + type(a) == type([]) # [unidiomatic-typecheck] + type(a) != type([]) # [unidiomatic-typecheck] + type(a) == type({}) # [unidiomatic-typecheck] + type(a) != type({}) # [unidiomatic-typecheck] + type(a) == type("") # [unidiomatic-typecheck] + type(a) != type("") # [unidiomatic-typecheck] diff --git a/tests/functional/u/unidiomatic_typecheck.txt b/tests/functional/u/unidiomatic_typecheck.txt index 84f5021d96..cc7e1902cc 100644 --- a/tests/functional/u/unidiomatic_typecheck.txt +++ b/tests/functional/u/unidiomatic_typecheck.txt @@ -6,9 +6,15 @@ unidiomatic-typecheck:12:4:12:20:simple_inference_positives:Use isinstance() rat unidiomatic-typecheck:13:4:13:24:simple_inference_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED unidiomatic-typecheck:14:4:14:20:simple_inference_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED unidiomatic-typecheck:15:4:15:20:simple_inference_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED -unidiomatic-typecheck:65:4:65:23:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED -unidiomatic-typecheck:66:4:66:27:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED -unidiomatic-typecheck:67:4:67:23:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED -unidiomatic-typecheck:68:4:68:27:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED -unidiomatic-typecheck:69:4:69:23:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED -unidiomatic-typecheck:70:4:70:27:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED +unidiomatic-typecheck:73:4:73:23:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED +unidiomatic-typecheck:74:4:74:27:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED +unidiomatic-typecheck:75:4:75:23:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED +unidiomatic-typecheck:76:4:76:27:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED +unidiomatic-typecheck:77:4:77:23:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED +unidiomatic-typecheck:78:4:78:27:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED +unidiomatic-typecheck:79:4:79:23:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED +unidiomatic-typecheck:80:4:80:23:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED +unidiomatic-typecheck:81:4:81:23:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED +unidiomatic-typecheck:82:4:82:23:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED +unidiomatic-typecheck:83:4:83:23:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED +unidiomatic-typecheck:84:4:84:23:type_of_literals_positives:Use isinstance() rather than type() for a typecheck.:UNDEFINED diff --git a/towncrier.toml b/towncrier.toml index 95c89ef963..a497b544e9 100644 --- a/towncrier.toml +++ b/towncrier.toml @@ -1,5 +1,5 @@ [tool.towncrier] -version = "3.3.6" +version = "3.3.7" directory = "doc/whatsnew/fragments" filename = "doc/whatsnew/3/3.3/index.rst" template = "doc/whatsnew/fragments/_template.rst" pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy