diff --git a/ChangeLog b/ChangeLog index 92a8e89311..c8eaa28bda 100644 --- a/ChangeLog +++ b/ChangeLog @@ -10,6 +10,10 @@ Release date: TBA Put new features here and also in 'doc/whatsnew/2.14.rst' +* Exclude dunder calls on super() from raising ``unnecessary-dunder-call``. + + Closes #6074 + * Add new check ``unnecessary-dunder-call`` for unnecessary dunder method calls. Closes #5936 @@ -36,6 +40,12 @@ Release date: TBA * Fix bug where specifically enabling just ``await-outside-async`` was not possible. +* The ``set_config_directly`` decorator has been removed. + +* Added new message called ``duplicate-value`` which identifies duplicate values inside sets. + + Closes #5880 + .. Insert your changelog randomly, it will reduce merge conflicts (Ie. not necessarily at the end) @@ -49,6 +59,13 @@ Release date: TBA * functions & classes which contain both a docstring and an ellipsis. * A body which contains an ellipsis ``nodes.Expr`` node & at least one other statement. +* Fix crash for ``redefined-slots-in-subclass`` when the type of the slot is not a const or a string. + + Closes #6100 + +* Only raise ``not-callable`` when all the inferred values of a property are not callable. + + Closes #5931 What's New in Pylint 2.13.4? diff --git a/doc/conf.py b/doc/conf.py index e53d82c16a..6858ce2057 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -103,17 +103,18 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = "python_docs_theme" +html_theme = "furo" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -html_theme_options = { - "collapsiblesidebar": True, - "issues_url": "https://github.com/pycqa/pylint/issues/new", - "root_name": "PyCQA", - "root_url": "https://meta.pycqa.org/en/latest/", -} +# TO-DO: Disable thme options too see how Furo themes look in default config +# html_theme_options = { +# "collapsiblesidebar": True, +# "issues_url": "https://github.com/pycqa/pylint/issues/new", +# "root_name": "PyCQA", +# "root_url": "https://meta.pycqa.org/en/latest/", +# } # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] @@ -146,9 +147,10 @@ smartquotes = False # Custom sidebar templates, maps document names to template names. -html_sidebars = { - "**": ["localtoc.html", "globaltoc.html", "relations.html", "sourcelink.html"] -} +# Use Default Furo Sidebar +# html_sidebars = { +# "**": ["localtoc.html", "globaltoc.html", "relations.html", "sourcelink.html"] +# } # Additional templates that should be rendered to pages, maps page names to # template names. diff --git a/doc/data/messages/a/arguments-differ/bad.py b/doc/data/messages/a/arguments-differ/bad.py new file mode 100644 index 0000000000..17901b59f7 --- /dev/null +++ b/doc/data/messages/a/arguments-differ/bad.py @@ -0,0 +1,23 @@ +class Drink: + def mix(self, fluid_one, fluid_two): + return fluid_one + fluid_two + + +class Cocktail(Drink): + def mix(self, fluid_one, fluid_two, alcoholic_fluid_one): # [arguments-differ] + return fluid_one + fluid_two + alcoholic_fluid_one + + +class Car: + tank = 0 + + def fill_tank(self, gas): + self.tank += gas + + +class Airplane(Car): + kerosine_tank = 0 + + def fill_tank(self, gas, kerosine): # [arguments-differ] + self.tank += gas + self.kerosine_tank += kerosine diff --git a/doc/data/messages/a/arguments-differ/details.rst b/doc/data/messages/a/arguments-differ/details.rst new file mode 100644 index 0000000000..71bd672683 --- /dev/null +++ b/doc/data/messages/a/arguments-differ/details.rst @@ -0,0 +1,8 @@ +``argument-differ`` denotes an issue with the Liskov Substitution Principle. +This means that the code in question violates an important design principle which does not have +one single solution. We recommend to search online for the best solution in your case. + +To give some examples of potential solutions: +- Add the argument to the parent class +- Remove the inheritance completely +- Add default arguments to the child class diff --git a/doc/data/messages/a/arguments-differ/good.py b/doc/data/messages/a/arguments-differ/good.py new file mode 100644 index 0000000000..b23e2e0e48 --- /dev/null +++ b/doc/data/messages/a/arguments-differ/good.py @@ -0,0 +1,24 @@ +class Drink: + def mix(self, fluid_one, fluid_two): + return fluid_one + fluid_two + + +class Cocktail(Drink): + def mix(self, fluid_one, fluid_two, alcoholic_fluid_one="Beer") + return fluid_one + fluid_two + alcoholic_fluid_one + + +class Car: + tank = 0 + + def fill_tank(self, gas): + self.tank += gas + + +class Airplane: + tank = 0 + kerosine_tank = 0 + + def fill_tank(self, gas, kerosine): + self.tank += gas + self.kerosine_tank += kerosine diff --git a/doc/data/messages/a/arguments-differ/related.rst b/doc/data/messages/a/arguments-differ/related.rst new file mode 100644 index 0000000000..db005e358b --- /dev/null +++ b/doc/data/messages/a/arguments-differ/related.rst @@ -0,0 +1 @@ +- `Liskov Substitution Principle `_ diff --git a/doc/data/messages/a/arguments-out-of-order/bad.py b/doc/data/messages/a/arguments-out-of-order/bad.py new file mode 100644 index 0000000000..06f6896e67 --- /dev/null +++ b/doc/data/messages/a/arguments-out-of-order/bad.py @@ -0,0 +1,13 @@ +def function_3_args(first_argument, second_argument, third_argument): + """Three arguments function""" + return first_argument, second_argument, third_argument + + +def args_out_of_order(): + first_argument = 1 + second_argument = 2 + third_argument = 3 + + function_3_args( # [arguments-out-of-order] + first_argument, third_argument, second_argument + ) diff --git a/doc/data/messages/a/arguments-out-of-order/good.py b/doc/data/messages/a/arguments-out-of-order/good.py new file mode 100644 index 0000000000..06ce7263f0 --- /dev/null +++ b/doc/data/messages/a/arguments-out-of-order/good.py @@ -0,0 +1,11 @@ +def function_3_args(first_argument, second_argument, third_argument): + """Three arguments function""" + return first_argument, second_argument, third_argument + + +def args_out_of_order(): + first_argument = 1 + second_argument = 2 + third_argument = 3 + + function_3_args(first_argument, second_argument, third_argument) diff --git a/doc/data/messages/a/arguments-renamed/bad.py b/doc/data/messages/a/arguments-renamed/bad.py new file mode 100644 index 0000000000..87b45a0d5b --- /dev/null +++ b/doc/data/messages/a/arguments-renamed/bad.py @@ -0,0 +1,13 @@ +class Fruit: + def brew(self, ingredient_name: str): + print(f"Brewing a {type(self)} with {ingredient_name}") + +class Apple(Fruit): + ... + +class Orange(Fruit): + def brew(self, flavor: str): # [arguments-renamed] + print(f"Brewing an orange with {flavor}") + +for fruit, ingredient_name in [[Orange(), "thyme"], [Apple(), "cinnamon"]]: + fruit.brew(ingredient_name=ingredient_name) diff --git a/doc/data/messages/a/arguments-renamed/good.py b/doc/data/messages/a/arguments-renamed/good.py new file mode 100644 index 0000000000..0cd1db7560 --- /dev/null +++ b/doc/data/messages/a/arguments-renamed/good.py @@ -0,0 +1,13 @@ +class Fruit: + def brew(self, ingredient_name: str): + print(f"Brewing a {type(self)} with {ingredient_name}") + +class Apple(Fruit): + ... + +class Orange(Fruit): + def brew(self, ingredient_name: str): + print(f"Brewing an orange with {ingredient_name}") + +for fruit, ingredient_name in [[Orange(), "thyme"], [Apple(), "cinnamon"]]: + fruit.brew(ingredient_name=ingredient_name) diff --git a/doc/data/messages/b/broad-except/bad.py b/doc/data/messages/b/broad-except/bad.py new file mode 100644 index 0000000000..f4946093e8 --- /dev/null +++ b/doc/data/messages/b/broad-except/bad.py @@ -0,0 +1,4 @@ +try: + 1 / 0 +except Exception: # [broad-except] + pass diff --git a/doc/data/messages/b/broad-except/good.py b/doc/data/messages/b/broad-except/good.py new file mode 100644 index 0000000000..b02b365b06 --- /dev/null +++ b/doc/data/messages/b/broad-except/good.py @@ -0,0 +1,4 @@ +try: + 1 / 0 +except ZeroDivisionError: + pass diff --git a/doc/data/messages/c/catching-non-exception/bad.py b/doc/data/messages/c/catching-non-exception/bad.py new file mode 100644 index 0000000000..86fa07b8a8 --- /dev/null +++ b/doc/data/messages/c/catching-non-exception/bad.py @@ -0,0 +1,8 @@ +class FooError: + pass + + +try: + 1 / 0 +except FooError: # [catching-non-exception] + pass diff --git a/doc/data/messages/c/catching-non-exception/good.py b/doc/data/messages/c/catching-non-exception/good.py new file mode 100644 index 0000000000..342fadab0e --- /dev/null +++ b/doc/data/messages/c/catching-non-exception/good.py @@ -0,0 +1,8 @@ +class FooError(Exception): + pass + + +try: + 1 / 0 +except FooError: + pass diff --git a/doc/data/messages/d/duplicate-value/bad.py b/doc/data/messages/d/duplicate-value/bad.py new file mode 100644 index 0000000000..bf6f8ad005 --- /dev/null +++ b/doc/data/messages/d/duplicate-value/bad.py @@ -0,0 +1 @@ +incorrect_set = {'value1', 23, 5, 'value1'} # [duplicate-value] diff --git a/doc/data/messages/d/duplicate-value/good.py b/doc/data/messages/d/duplicate-value/good.py new file mode 100644 index 0000000000..c30cd78213 --- /dev/null +++ b/doc/data/messages/d/duplicate-value/good.py @@ -0,0 +1 @@ +correct_set = {'value1', 23, 5} diff --git a/doc/data/messages/m/missing-format-argument-key/bad.py b/doc/data/messages/m/missing-format-argument-key/bad.py new file mode 100644 index 0000000000..152c3939b1 --- /dev/null +++ b/doc/data/messages/m/missing-format-argument-key/bad.py @@ -0,0 +1 @@ +print("My name is {first} {last}".format(first="John")) # [missing-format-argument-key] diff --git a/doc/data/messages/m/missing-format-argument-key/good.py b/doc/data/messages/m/missing-format-argument-key/good.py new file mode 100644 index 0000000000..6570af146a --- /dev/null +++ b/doc/data/messages/m/missing-format-argument-key/good.py @@ -0,0 +1 @@ +print("My name is {first} {last}".format(first="John", last="Wick")) diff --git a/doc/data/messages/m/missing-format-argument-key/related.rst b/doc/data/messages/m/missing-format-argument-key/related.rst new file mode 100644 index 0000000000..b424943c31 --- /dev/null +++ b/doc/data/messages/m/missing-format-argument-key/related.rst @@ -0,0 +1,2 @@ +* `PEP 3101 `_ +* `Custom String Formmating `_ diff --git a/doc/data/messages/s/super-with-arguments/bad.py b/doc/data/messages/s/super-with-arguments/bad.py new file mode 100644 index 0000000000..a5adf26f2f --- /dev/null +++ b/doc/data/messages/s/super-with-arguments/bad.py @@ -0,0 +1,7 @@ +class Fruit: + pass + + +class Orange(Fruit): + def __init__(self): + super(Orange, self).__init__() # [super-with-arguments] diff --git a/doc/data/messages/s/super-with-arguments/good.py b/doc/data/messages/s/super-with-arguments/good.py new file mode 100644 index 0000000000..d925fabbd9 --- /dev/null +++ b/doc/data/messages/s/super-with-arguments/good.py @@ -0,0 +1,7 @@ +class Fruit: + pass + + +class Orange(Fruit): + def __init__(self): + super().__init__() diff --git a/doc/data/messages/t/too-few-format-args/bad.py b/doc/data/messages/t/too-few-format-args/bad.py new file mode 100644 index 0000000000..03fa3de6d5 --- /dev/null +++ b/doc/data/messages/t/too-few-format-args/bad.py @@ -0,0 +1 @@ +print("Today is {0}, so tomorrow will be {1}".format("Monday")) # [too-few-format-args] diff --git a/doc/data/messages/t/too-few-format-args/good.py b/doc/data/messages/t/too-few-format-args/good.py new file mode 100644 index 0000000000..c6b2a4867f --- /dev/null +++ b/doc/data/messages/t/too-few-format-args/good.py @@ -0,0 +1 @@ +print("Today is {0}, so tomorrow will be {1}".format("Monday", "Tuesday")) diff --git a/doc/data/messages/t/too-few-format-args/related.rst b/doc/data/messages/t/too-few-format-args/related.rst new file mode 100644 index 0000000000..d9a8bdbe62 --- /dev/null +++ b/doc/data/messages/t/too-few-format-args/related.rst @@ -0,0 +1 @@ +`String Formmating `_ diff --git a/doc/data/messages/t/too-many-arguments/bad.py b/doc/data/messages/t/too-many-arguments/bad.py new file mode 100644 index 0000000000..5f8d6e0b05 --- /dev/null +++ b/doc/data/messages/t/too-many-arguments/bad.py @@ -0,0 +1,16 @@ +def three_d_chess_move( # [too-many-arguments] + x_white, + y_white, + z_white, + piece_white, + x_black, + y_black, + z_black, + piece_black, + x_blue, + y_blue, + z_blue, + piece_blue, + current_player, +): + pass diff --git a/doc/data/messages/t/too-many-arguments/good.py b/doc/data/messages/t/too-many-arguments/good.py new file mode 100644 index 0000000000..f3ea3c8ebb --- /dev/null +++ b/doc/data/messages/t/too-many-arguments/good.py @@ -0,0 +1,18 @@ +from dataclasses import dataclass + + +@dataclass +class ThreeDChessPiece: + x: int + y: int + z: int + type: str + + +def three_d_chess_move( + white: ThreeDChessPiece, + black: ThreeDChessPiece, + blue: ThreeDChessPiece, + current_player, +): + pass diff --git a/doc/data/messages/t/too-many-format-args/bad.py b/doc/data/messages/t/too-many-format-args/bad.py new file mode 100644 index 0000000000..0c98c084d1 --- /dev/null +++ b/doc/data/messages/t/too-many-format-args/bad.py @@ -0,0 +1 @@ +print("Today is {0}, so tomorrow will be {1}".format("Monday", "Tuesday", "Wednesday")) # [too-many-format-args] diff --git a/doc/data/messages/t/too-many-format-args/good.py b/doc/data/messages/t/too-many-format-args/good.py new file mode 100644 index 0000000000..c6b2a4867f --- /dev/null +++ b/doc/data/messages/t/too-many-format-args/good.py @@ -0,0 +1 @@ +print("Today is {0}, so tomorrow will be {1}".format("Monday", "Tuesday")) diff --git a/doc/data/messages/t/too-many-format-args/related.rst b/doc/data/messages/t/too-many-format-args/related.rst new file mode 100644 index 0000000000..d9a8bdbe62 --- /dev/null +++ b/doc/data/messages/t/too-many-format-args/related.rst @@ -0,0 +1 @@ +`String Formmating `_ diff --git a/doc/requirements.txt b/doc/requirements.txt index 4528f5da4b..8292d884c2 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,3 +1,3 @@ Sphinx==4.5.0 -python-docs-theme==2022.1 +furo==2022.3.4 -e . diff --git a/doc/whatsnew/2.14.rst b/doc/whatsnew/2.14.rst index 44557190b0..f17493b089 100644 --- a/doc/whatsnew/2.14.rst +++ b/doc/whatsnew/2.14.rst @@ -27,6 +27,10 @@ New checkers Closes #4525 +* Added new message called ``duplicate-value`` which identifies duplicate values inside sets. + + Closes #5880 + Removed checkers ================ @@ -38,8 +42,14 @@ Extensions Other Changes ============= +* Only raise ``not-callable`` when all the inferred values of a property are not callable. + + Closes #5931 + * The concept of checker priority has been removed. +* The ``set_config_directly`` decorator has been removed. + * Fix false negative for ``no-member`` when attempting to assign an instance attribute to itself without any prior assignment. diff --git a/pylint/checkers/base/basic_checker.py b/pylint/checkers/base/basic_checker.py index 1e5a2a15bb..55ce39d900 100644 --- a/pylint/checkers/base/basic_checker.py +++ b/pylint/checkers/base/basic_checker.py @@ -15,7 +15,7 @@ from pylint import interfaces from pylint import utils as lint_utils from pylint.checkers import BaseChecker, utils -from pylint.interfaces import IAstroidChecker +from pylint.interfaces import HIGH, IAstroidChecker from pylint.reporters.ureports import nodes as reporter_nodes from pylint.utils import LinterStats from pylint.utils.utils import get_global_option @@ -246,6 +246,11 @@ class BasicChecker(_BasicChecker): "Used when an assert statement has a string literal as its first argument, which will " "cause the assert to always pass.", ), + "W0130": ( + "Duplicate value %r in set", + "duplicate-value", + "This message is emitted when a set contains the same value two or more times.", + ), } reports = (("RP0101", "Statistics by type", report_by_type_stats),) @@ -637,6 +642,21 @@ def visit_dict(self, node: nodes.Dict) -> None: self.add_message("duplicate-key", node=node, args=key) keys.add(key) + @utils.check_messages("duplicate-value") + def visit_set(self, node: nodes.Set) -> None: + """Check duplicate value in set.""" + values = set() + for v in node.elts: + if isinstance(v, nodes.Const): + value = v.value + else: + continue + if value in values: + self.add_message( + "duplicate-value", node=node, args=value, confidence=HIGH + ) + values.add(value) + def visit_tryfinally(self, node: nodes.TryFinally) -> None: """Update try...finally flag.""" self._tryfinallys.append(node) diff --git a/pylint/checkers/base/docstring_checker.py b/pylint/checkers/base/docstring_checker.py index 73a6c31e50..ab480ab58b 100644 --- a/pylint/checkers/base/docstring_checker.py +++ b/pylint/checkers/base/docstring_checker.py @@ -101,6 +101,11 @@ class DocStringChecker(_BasicChecker): ), ) + def __init__( + self, linter=None, *, future_option_parsing: Literal[None, True] = None + ): + _BasicChecker.__init__(self, linter, future_option_parsing=True) + def open(self): self.linter.stats.reset_undocumented() diff --git a/pylint/checkers/classes/class_checker.py b/pylint/checkers/classes/class_checker.py index db5d6dbb1f..e8218ddef9 100644 --- a/pylint/checkers/classes/class_checker.py +++ b/pylint/checkers/classes/class_checker.py @@ -760,7 +760,7 @@ class ClassChecker(BaseChecker): ) def __init__(self, linter=None): - super().__init__(linter) + super().__init__(linter, future_option_parsing=True) self._accessed = ScopeAccessMap() self._first_attrs = [] self._meth_could_be_func = None @@ -1019,7 +1019,7 @@ def _check_attribute_defined_outside_init(self, cnode: nodes.ClassDef) -> None: # checks attributes are defined in an allowed method such as __init__ if not self.linter.is_message_enabled("attribute-defined-outside-init"): return - defining_methods = self.config.defining_attr_methods + defining_methods = self.linter.namespace.defining_attr_methods current_module = cnode.root() for attr, nodes_lst in cnode.instance_attrs.items(): # Exclude `__dict__` as it is already defined. @@ -1380,8 +1380,9 @@ def _check_redefined_slots( slots_names.append(slot.value) else: inferred_slot = safe_infer(slot) - if inferred_slot: - slots_names.append(inferred_slot.value) + inferred_slot_value = getattr(inferred_slot, "value", None) + if isinstance(inferred_slot_value, str): + slots_names.append(inferred_slot_value) # Slots of all parent classes ancestors_slots_names = { @@ -1629,7 +1630,7 @@ def _check_protected_attribute_access(self, node: nodes.Attribute): if ( is_attr_protected(attrname) - and attrname not in self.config.exclude_protected + and attrname not in self.linter.namespace.exclude_protected ): klass = node_frame_class(node) @@ -1700,7 +1701,7 @@ def _check_protected_attribute_access(self, node: nodes.Attribute): licit_protected_member = not attrname.startswith("__") if ( - not self.config.check_protected_access_in_special_methods + not self.linter.namespace.check_protected_access_in_special_methods and licit_protected_member and self._is_called_inside_special_method(node) ): @@ -1855,8 +1856,9 @@ def _check_first_arg_for_type(self, node, metaclass=0): if node.type == "staticmethod": if ( first_arg == "self" - or first_arg in self.config.valid_classmethod_first_arg - or first_arg in self.config.valid_metaclass_classmethod_first_arg + or first_arg in self.linter.namespace.valid_classmethod_first_arg + or first_arg + in self.linter.namespace.valid_metaclass_classmethod_first_arg ): self.add_message("bad-staticmethod-argument", args=first, node=node) return @@ -1870,7 +1872,7 @@ def _check_first_arg_for_type(self, node, metaclass=0): if node.type == "classmethod": self._check_first_arg_config( first, - self.config.valid_metaclass_classmethod_first_arg, + self.linter.namespace.valid_metaclass_classmethod_first_arg, node, "bad-mcs-classmethod-argument", node.name, @@ -1879,7 +1881,7 @@ def _check_first_arg_for_type(self, node, metaclass=0): else: self._check_first_arg_config( first, - self.config.valid_classmethod_first_arg, + self.linter.namespace.valid_classmethod_first_arg, node, "bad-mcs-method-argument", node.name, @@ -1888,7 +1890,7 @@ def _check_first_arg_for_type(self, node, metaclass=0): elif node.type == "classmethod" or node.name == "__class_getitem__": self._check_first_arg_config( first, - self.config.valid_classmethod_first_arg, + self.linter.namespace.valid_classmethod_first_arg, node, "bad-classmethod-argument", node.name, diff --git a/pylint/checkers/design_analysis.py b/pylint/checkers/design_analysis.py index 0b3097924c..3a1de40165 100644 --- a/pylint/checkers/design_analysis.py +++ b/pylint/checkers/design_analysis.py @@ -403,7 +403,7 @@ class MisdesignChecker(BaseChecker): ) def __init__(self, linter=None): - super().__init__(linter) + super().__init__(linter, future_option_parsing=True) self._returns = None self._branches = None self._stmts = None @@ -435,21 +435,22 @@ def _ignored_argument_names(self): def visit_classdef(self, node: nodes.ClassDef) -> None: """Check size of inheritance hierarchy and number of instance attributes.""" parents = _get_parents( - node, STDLIB_CLASSES_IGNORE_ANCESTOR.union(self.config.ignored_parents) + node, + STDLIB_CLASSES_IGNORE_ANCESTOR.union(self.linter.namespace.ignored_parents), ) nb_parents = len(parents) - if nb_parents > self.config.max_parents: + if nb_parents > self.linter.namespace.max_parents: self.add_message( "too-many-ancestors", node=node, - args=(nb_parents, self.config.max_parents), + args=(nb_parents, self.linter.namespace.max_parents), ) - if len(node.instance_attrs) > self.config.max_attributes: + if len(node.instance_attrs) > self.linter.namespace.max_attributes: self.add_message( "too-many-instance-attributes", node=node, - args=(len(node.instance_attrs), self.config.max_attributes), + args=(len(node.instance_attrs), self.linter.namespace.max_attributes), ) @check_messages("too-few-public-methods", "too-many-public-methods") @@ -466,11 +467,11 @@ def leave_classdef(self, node: nodes.ClassDef) -> None: # for classes such as unittest.TestCase, which provides # a lot of assert methods. It doesn't make sense to warn # when the user subclasses TestCase to add his own tests. - if my_methods > self.config.max_public_methods: + if my_methods > self.linter.namespace.max_public_methods: self.add_message( "too-many-public-methods", node=node, - args=(my_methods, self.config.max_public_methods), + args=(my_methods, self.linter.namespace.max_public_methods), ) # Stop here if the class is excluded via configuration. @@ -491,11 +492,11 @@ def leave_classdef(self, node: nodes.ClassDef) -> None: # This checks all the methods defined by ancestors and # by the current class. all_methods = _count_methods_in_class(node) - if all_methods < self.config.min_public_methods: + if all_methods < self.linter.namespace.min_public_methods: self.add_message( "too-few-public-methods", node=node, - args=(all_methods, self.config.min_public_methods), + args=(all_methods, self.linter.namespace.min_public_methods), ) @check_messages( @@ -523,19 +524,21 @@ def visit_functiondef(self, node: nodes.FunctionDef) -> None: ) argnum = len(args) - ignored_args_num - if argnum > self.config.max_args: + if argnum > self.linter.namespace.max_args: self.add_message( "too-many-arguments", node=node, - args=(len(args), self.config.max_args), + args=(len(args), self.linter.namespace.max_args), ) else: ignored_args_num = 0 # check number of local variables locnum = len(node.locals) - ignored_args_num - if locnum > self.config.max_locals: + if locnum > self.linter.namespace.max_locals: self.add_message( - "too-many-locals", node=node, args=(locnum, self.config.max_locals) + "too-many-locals", + node=node, + args=(locnum, self.linter.namespace.max_locals), ) # init new statements counter self._stmts.append(1) @@ -554,26 +557,26 @@ def leave_functiondef(self, node: nodes.FunctionDef) -> None: checks for max returns, branch, return in __init__ """ returns = self._returns.pop() - if returns > self.config.max_returns: + if returns > self.linter.namespace.max_returns: self.add_message( "too-many-return-statements", node=node, - args=(returns, self.config.max_returns), + args=(returns, self.linter.namespace.max_returns), ) branches = self._branches[node] - if branches > self.config.max_branches: + if branches > self.linter.namespace.max_branches: self.add_message( "too-many-branches", node=node, - args=(branches, self.config.max_branches), + args=(branches, self.linter.namespace.max_branches), ) # check number of statements stmts = self._stmts.pop() - if stmts > self.config.max_statements: + if stmts > self.linter.namespace.max_statements: self.add_message( "too-many-statements", node=node, - args=(stmts, self.config.max_statements), + args=(stmts, self.linter.namespace.max_statements), ) leave_asyncfunctiondef = leave_functiondef @@ -625,11 +628,11 @@ def _check_boolean_expressions(self, node): if not isinstance(condition, astroid.BoolOp): return nb_bool_expr = _count_boolean_expressions(condition) - if nb_bool_expr > self.config.max_bool_expr: + if nb_bool_expr > self.linter.namespace.max_bool_expr: self.add_message( "too-many-boolean-expressions", node=condition, - args=(nb_bool_expr, self.config.max_bool_expr), + args=(nb_bool_expr, self.linter.namespace.max_bool_expr), ) def visit_while(self, node: nodes.While) -> None: diff --git a/pylint/checkers/dunder_methods.py b/pylint/checkers/dunder_methods.py index 4c260c34a5..0e7beb16a5 100644 --- a/pylint/checkers/dunder_methods.py +++ b/pylint/checkers/dunder_methods.py @@ -23,6 +23,9 @@ class DunderCallChecker(BaseChecker): __setstate__, __reduce__, __reduce_ex__ since these either have no alternative method of being called or have a genuine use case for being called manually. + + Additionally we exclude dunder method calls on super() since + these can't be written in an alternative manner. """ __implements__ = IAstroidChecker @@ -138,6 +141,11 @@ def visit_call(self, node: nodes.Call) -> None: if ( isinstance(node.func, nodes.Attribute) and node.func.attrname in self.includedict + and not ( + isinstance(node.func.expr, nodes.Call) + and isinstance(node.func.expr.func, nodes.Name) + and node.func.expr.func.name == "super" + ) ): self.add_message( "unnecessary-dunder-call", diff --git a/pylint/checkers/exceptions.py b/pylint/checkers/exceptions.py index bd9d9ec9ed..6016320b3f 100644 --- a/pylint/checkers/exceptions.py +++ b/pylint/checkers/exceptions.py @@ -245,6 +245,9 @@ class ExceptionsChecker(checkers.BaseChecker): ), ) + def __init__(self, linter: "PyLinter") -> None: + super().__init__(linter, future_option_parsing=True) + def open(self): self._builtin_exceptions = _builtin_exceptions() super().open() @@ -523,7 +526,7 @@ def visit_tryexcept(self, node: nodes.TryExcept) -> None: "bad-except-order", node=handler.type, args=msg ) if ( - exception.name in self.config.overgeneral_exceptions + exception.name in self.linter.namespace.overgeneral_exceptions and exception.root().name == utils.EXCEPTIONS_MODULE and not _is_raising(handler.body) ): diff --git a/pylint/checkers/format.py b/pylint/checkers/format.py index 70c56a621a..150d95f1fa 100644 --- a/pylint/checkers/format.py +++ b/pylint/checkers/format.py @@ -301,7 +301,7 @@ class FormatChecker(BaseTokenChecker): ) def __init__(self, linter=None): - super().__init__(linter) + super().__init__(linter, future_option_parsing=True) self._lines = None self._visited_lines = None self._bracket_stack = [None] @@ -498,7 +498,7 @@ def process_tokens(self, tokens): handler(tokens, idx) line_num -= 1 # to be ok with "wc -l" - if line_num > self.config.max_module_lines: + if line_num > self.linter.namespace.max_module_lines: # Get the line where the too-many-lines (or its message id) # was disabled or default to 1. message_definition = self.linter.msgs_store.get_message_definitions( @@ -511,7 +511,7 @@ def process_tokens(self, tokens): ) self.add_message( "too-many-lines", - args=(line_num, self.config.max_module_lines), + args=(line_num, self.linter.namespace.max_module_lines), line=line, ) @@ -532,7 +532,7 @@ def _check_line_ending(self, line_ending, line_num): self._last_line_ending = line_ending # check if line ending is as expected - expected = self.config.expected_line_ending_format + expected = self.linter.namespace.expected_line_ending_format if expected: # reduce multiple \n\n\n\n to one \n line_ending = reduce(lambda x, y: x + y if x != y else x, line_ending, "") @@ -601,13 +601,13 @@ def _check_multi_statement_line(self, node, line): if ( isinstance(node.parent, nodes.If) and not node.parent.orelse - and self.config.single_line_if_stmt + and self.linter.namespace.single_line_if_stmt ): return if ( isinstance(node.parent, nodes.ClassDef) and len(node.parent.body) == 1 - and self.config.single_line_class_stmt + and self.linter.namespace.single_line_class_stmt ): return @@ -638,8 +638,8 @@ def check_line_ending(self, line: str, i: int) -> None: def check_line_length(self, line: str, i: int, checker_off: bool) -> None: """Check that the line length is less than the authorized value.""" - max_chars = self.config.max_line_length - ignore_long_line = self.config.ignore_long_lines + max_chars = self.linter.namespace.max_line_length + ignore_long_line = self.linter.namespace.ignore_long_lines line = line.rstrip() if len(line) > max_chars and not ignore_long_line.search(line): if checker_off: @@ -673,10 +673,8 @@ def is_line_length_check_activated(pylint_pattern_match_object) -> bool: def specific_splitlines(lines: str) -> List[str]: """Split lines according to universal newlines except those in a specific sets.""" unsplit_ends = { - "\v", - "\x0b", - "\f", - "\x0c", + "\x0b", # synonym of \v + "\x0c", # synonym of \f "\x1c", "\x1d", "\x1e", @@ -710,7 +708,7 @@ def check_lines(self, lines: str, lineno: int) -> None: # we'll also handle the line ending check here to avoid double-iteration # unless the line lengths are suspect - max_chars = self.config.max_line_length + max_chars = self.linter.namespace.max_line_length split_lines = self.specific_splitlines(lines) @@ -747,7 +745,7 @@ def check_lines(self, lines: str, lineno: int) -> None: def check_indent_level(self, string, expected, line_num): """Return the indent level of the string.""" - indent = self.config.indent_string + indent = self.linter.namespace.indent_string if indent == "\\t": # \t is not interpreted in the configuration file indent = "\t" level = 0 diff --git a/pylint/checkers/imports.py b/pylint/checkers/imports.py index 491479d7cd..1a23b7947d 100644 --- a/pylint/checkers/imports.py +++ b/pylint/checkers/imports.py @@ -7,7 +7,7 @@ import collections import copy import os -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union +from typing import TYPE_CHECKING, Any, Dict, List, Set, Tuple, Union import astroid from astroid import nodes @@ -383,8 +383,8 @@ class ImportsChecker(DeprecatedMixin, BaseChecker): ), ) - def __init__(self, linter: Optional["PyLinter"] = None) -> None: - BaseChecker.__init__(self, linter) + def __init__(self, linter: "PyLinter") -> None: + BaseChecker.__init__(self, linter, future_option_parsing=True) self.import_graph: collections.defaultdict = collections.defaultdict(set) self._imports_stack: List[Tuple[Any, Any]] = [] self._first_non_import_node = None @@ -408,10 +408,10 @@ def open(self): # Build a mapping {'module': 'preferred-module'} self.preferred_modules = dict( module.split(":") - for module in self.config.preferred_modules + for module in self.linter.namespace.preferred_modules if ":" in module ) - self._allow_any_import_level = set(self.config.allow_any_import_level) + self._allow_any_import_level = set(self.linter.namespace.allow_any_import_level) def _import_graph_without_ignored_edges(self): filtered_graph = copy.deepcopy(self.import_graph) @@ -429,7 +429,7 @@ def close(self): def deprecated_modules(self): """Callback returning the deprecated modules.""" - return self.config.deprecated_modules + return self.linter.namespace.deprecated_modules @check_messages(*MSGS) def visit_import(self, node: nodes.Import) -> None: @@ -651,7 +651,7 @@ def _check_imports_order(self, _module_node): third_party_not_ignored = [] first_party_not_ignored = [] local_not_ignored = [] - isort_driver = IsortDriver(self.config) + isort_driver = IsortDriver(self.linter.namespace) for node, modname in self._imports_stack: if modname.startswith("."): package = "." + modname.split(".")[1] @@ -748,8 +748,9 @@ def _get_imported_module(self, importnode, modname): return None if _ignore_import_failure(importnode, modname, self._ignored_modules): return None - if not self.config.analyse_fallback_blocks and is_from_fallback_block( - importnode + if ( + not self.linter.namespace.analyse_fallback_blocks + and is_from_fallback_block(importnode) ): return None @@ -865,18 +866,18 @@ def _report_dependencies_graph(self, sect, _, _dummy): """Write dependencies as a dot (graphviz) file.""" dep_info = self.linter.stats.dependencies if not dep_info or not ( - self.config.import_graph - or self.config.ext_import_graph - or self.config.int_import_graph + self.linter.namespace.import_graph + or self.linter.namespace.ext_import_graph + or self.linter.namespace.int_import_graph ): raise EmptyReportError() - filename = self.config.import_graph + filename = self.linter.namespace.import_graph if filename: _make_graph(filename, dep_info, sect, "") - filename = self.config.ext_import_graph + filename = self.linter.namespace.ext_import_graph if filename: _make_graph(filename, self._external_dependencies_info(), sect, "external ") - filename = self.config.int_import_graph + filename = self.linter.namespace.int_import_graph if filename: _make_graph(filename, self._internal_dependencies_info(), sect, "internal ") @@ -917,7 +918,7 @@ def _check_wildcard_imports(self, node, imported_module): def _wildcard_import_is_allowed(self, imported_module): return ( - self.config.allow_wildcard_with_all + self.linter.namespace.allow_wildcard_with_all and imported_module is not None and "__all__" in imported_module.locals ) diff --git a/pylint/checkers/misc.py b/pylint/checkers/misc.py index e5e77bb81d..75ed50600e 100644 --- a/pylint/checkers/misc.py +++ b/pylint/checkers/misc.py @@ -91,16 +91,22 @@ class EncodingChecker(BaseChecker): "type": "string", "metavar": "", "help": "Regular expression of note tags to take in consideration.", + "default": "", }, ), ) + def __init__(self, linter: "PyLinter") -> None: + super().__init__(linter, future_option_parsing=True) + def open(self): super().open() - notes = "|".join(re.escape(note) for note in self.config.notes) - if self.config.notes_rgx: - regex_string = rf"#\s*({notes}|{self.config.notes_rgx})(?=(:|\s|\Z))" + notes = "|".join(re.escape(note) for note in self.linter.namespace.notes) + if self.linter.namespace.notes_rgx: + regex_string = ( + rf"#\s*({notes}|{self.linter.namespace.notes_rgx})(?=(:|\s|\Z))" + ) else: regex_string = rf"#\s*({notes})(?=(:|\s|\Z))" @@ -133,7 +139,7 @@ def process_module(self, node: nodes.Module) -> None: def process_tokens(self, tokens): """Inspect the source to find fixme problems.""" - if not self.config.notes: + if not self.linter.namespace.notes: return comments = ( token_info for token_info in tokens if token_info.type == tokenize.COMMENT @@ -156,7 +162,7 @@ def process_tokens(self, tokens): except PragmaParserError: # Printing useful information dealing with this error is done in the lint package pass - if set(values) & set(self.config.notes): + if set(values) & set(self.linter.namespace.notes): continue except ValueError: self.add_message( diff --git a/pylint/checkers/refactoring/refactoring_checker.py b/pylint/checkers/refactoring/refactoring_checker.py index 715d3db7c7..c9f78f68db 100644 --- a/pylint/checkers/refactoring/refactoring_checker.py +++ b/pylint/checkers/refactoring/refactoring_checker.py @@ -1278,7 +1278,7 @@ def _find_lower_upper_bounds(comparison_node, uses): if isinstance(comparison_node, nodes.Compare): _find_lower_upper_bounds(comparison_node, uses) - for _, bounds in uses.items(): + for bounds in uses.values(): num_shared = len(bounds["lower_bound"].intersection(bounds["upper_bound"])) num_lower_bounds = len(bounds["lower_bound"]) num_upper_bounds = len(bounds["upper_bound"]) diff --git a/pylint/checkers/strings.py b/pylint/checkers/strings.py index dd9f1887f4..a650650675 100644 --- a/pylint/checkers/strings.py +++ b/pylint/checkers/strings.py @@ -677,8 +677,8 @@ class StringConstantChecker(BaseTokenChecker): # Unicode strings. UNICODE_ESCAPE_CHARACTERS = "uUN" - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, linter): + super().__init__(linter, future_option_parsing=True) self.string_tokens = {} # token position -> (token value, next token) def process_module(self, node: nodes.Module) -> None: @@ -709,7 +709,7 @@ 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: + if self.linter.namespace.check_quote_consistency: self.check_for_consistent_string_delimiters(tokens) @check_messages("implicit-str-concat") @@ -783,7 +783,7 @@ def check_for_concatenated_strings(self, elements, iterable_type): if matching_token != elt.value and next_token is not None: if next_token.type == tokenize.STRING and ( next_token.start[0] == elt.lineno - or self.config.check_str_concat_over_line_jumps + or self.linter.namespace.check_str_concat_over_line_jumps ): self.add_message( "implicit-str-concat", line=elt.lineno, args=(iterable_type,) diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index c02fe920d1..98af20e53a 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -883,6 +883,9 @@ class TypeChecker(BaseChecker): ), ) + def __init__(self, linter: "PyLinter") -> None: + super().__init__(linter, future_option_parsing=True) + def open(self) -> None: py_version = get_global_option(self, "py-version") self._py310_plus = py_version >= (3, 10) @@ -899,7 +902,7 @@ def _compiled_generated_members(self) -> Tuple[Pattern, ...]: # (surrounded by quote `"` and followed by a comma `,`) # REQUEST,aq_parent,"[a-zA-Z]+_set{1,2}"' => # ('REQUEST', 'aq_parent', '[a-zA-Z]+_set{1,2}') - generated_members = self.config.generated_members + generated_members = self.linter.namespace.generated_members if isinstance(generated_members, str): gen = shlex.shlex(generated_members) gen.whitespace += "," @@ -986,7 +989,7 @@ def visit_attribute(self, node: nodes.Attribute) -> None: ] if ( len(non_opaque_inference_results) != len(inferred) - and self.config.ignore_on_opaque_inference + and self.linter.namespace.ignore_on_opaque_inference ): # There is an ambiguity in the inference. Since we can't # make sure that we won't emit a false positive, we just stop @@ -995,7 +998,10 @@ def visit_attribute(self, node: nodes.Attribute) -> None: for owner in non_opaque_inference_results: name = getattr(owner, "name", None) if _is_owner_ignored( - owner, name, self.config.ignored_classes, self.config.ignored_modules + owner, + name, + self.linter.namespace.ignored_classes, + self.linter.namespace.ignored_modules, ): continue @@ -1024,8 +1030,8 @@ def visit_attribute(self, node: nodes.Attribute) -> None: owner, name, self._mixin_class_rgx, - ignored_mixins=self.config.ignore_mixin_members, - ignored_none=self.config.ignore_none, + ignored_mixins=self.linter.namespace.ignore_mixin_members, + ignored_none=self.linter.namespace.ignore_none, ): continue missingattr.add((owner, name)) @@ -1080,12 +1086,12 @@ def _get_nomember_msgid_hint(self, node, owner): hint = "" else: msg = "no-member" - if self.config.missing_member_hint: + if self.linter.namespace.missing_member_hint: hint = _missing_member_hint( owner, node.attrname, - self.config.missing_member_hint_distance, - self.config.missing_member_max_choices, + self.linter.namespace.missing_member_hint_distance, + self.linter.namespace.missing_member_max_choices, ) else: hint = "" @@ -1226,20 +1232,22 @@ def _check_uninferable_call(self, node): # Decorated, see if it is decorated with a property. # Also, check the returns and see if they are callable. if decorated_with_property(attr): - try: - all_returns_are_callable = all( - return_node.callable() or return_node is astroid.Uninferable - for return_node in attr.infer_call_result(node) - ) + call_results = list(attr.infer_call_result(node)) except astroid.InferenceError: continue - if not all_returns_are_callable: - self.add_message( - "not-callable", node=node, args=node.func.as_string() - ) - break + if all( + return_node is astroid.Uninferable for return_node in call_results + ): + # We were unable to infer return values of the call, skipping + continue + + if any(return_node.callable() for return_node in call_results): + # Only raise this issue if *all* the inferred values are not callable + continue + + self.add_message("not-callable", node=node, args=node.func.as_string()) def _check_argument_order(self, node, call_site, called, called_param_names): """Match the supplied argument names against the function parameters. @@ -1333,7 +1341,7 @@ def visit_call(self, node: nodes.Call) -> None: # Has the function signature changed in ways we cannot reliably detect? if hasattr(called, "decorators") and decorated_with( - called, self.config.signature_mutators + called, self.linter.namespace.signature_mutators ): return @@ -1707,7 +1715,7 @@ def visit_with(self, node: nodes.With) -> None: # Check if we are dealing with a function decorated # with contextlib.contextmanager. if decorated_with( - inferred.parent, self.config.contextmanager_decorators + inferred.parent, self.linter.namespace.contextmanager_decorators ): continue # If the parent of the generator is not the context manager itself, @@ -1735,7 +1743,9 @@ def visit_with(self, node: nodes.With) -> None: scope = inferred_path.scope() if not isinstance(scope, nodes.FunctionDef): continue - if decorated_with(scope, self.config.contextmanager_decorators): + if decorated_with( + scope, self.linter.namespace.contextmanager_decorators + ): break else: self.add_message( @@ -1752,7 +1762,7 @@ def visit_with(self, node: nodes.With) -> None: if not has_known_bases(inferred): continue # Just ignore mixin classes. - if self.config.ignore_mixin_members: + if self.linter.namespace.ignore_mixin_members: if inferred.name[-5:].lower() == "mixin": continue @@ -1997,6 +2007,9 @@ class IterableChecker(BaseChecker): ), } + def __init__(self, linter: "PyLinter") -> None: + super().__init__(linter, future_option_parsing=True) + @staticmethod def _is_asyncio_coroutine(node): if not isinstance(node, nodes.Call): diff --git a/pylint/config/__init__.py b/pylint/config/__init__.py index 95305efdbc..05b7568c01 100644 --- a/pylint/config/__init__.py +++ b/pylint/config/__init__.py @@ -10,6 +10,7 @@ from pylint.config.argument import _Argument from pylint.config.arguments_manager import _ArgumentsManager +from pylint.config.config_file_parser import _ConfigurationFileParser from pylint.config.configuration_mixin import ConfigurationMixIn from pylint.config.environment_variable import PYLINTRC from pylint.config.find_default_config_files import ( @@ -27,6 +28,7 @@ __all__ = [ "_Argument", "_ArgumentsManager", + "_ConfigurationFileParser", "ConfigurationMixIn", "find_default_config_files", "find_pylintrc", diff --git a/pylint/config/argument.py b/pylint/config/argument.py index bf35f1a854..bb9298cffa 100644 --- a/pylint/config/argument.py +++ b/pylint/config/argument.py @@ -8,24 +8,71 @@ """ -from typing import Any, Callable, Dict, List, Optional, Union +import argparse +import re +from typing import Callable, Dict, List, Optional, Pattern, Sequence, Union from pylint import utils as pylint_utils -_ArgumentTypes = Union[str, List[str]] +_ArgumentTypes = Union[ + str, Sequence[str], int, Pattern[str], bool, Sequence[Pattern[str]] +] """List of possible argument types.""" -def _csv_validator(value: Union[str, List[str]]) -> List[str]: - """Validates a comma separated string.""" +def _csv_transformer(value: str) -> Sequence[str]: + """Transforms a comma separated string.""" return pylint_utils._check_csv(value) -_ASSIGNMENT_VALIDATORS: Dict[str, Callable[[Any], _ArgumentTypes]] = { +YES_VALUES = {"y", "yes", "true"} +NO_VALUES = {"n", "no", "false"} + + +def _yn_transformer(value: str) -> bool: + """Transforms a yes/no or stringified bool into a bool.""" + value = value.lower() + if value in YES_VALUES: + return True + if value in NO_VALUES: + return False + raise argparse.ArgumentTypeError( + None, f"Invalid yn value '{value}', should be in {*YES_VALUES, *NO_VALUES}" + ) + + +def _non_empty_string_transformer(value: str) -> str: + """Check that a string is not empty and remove quotes.""" + if not value: + raise argparse.ArgumentTypeError("Option cannot be an empty string.") + return pylint_utils._unquote(value) + + +def _regexp_csv_transfomer(value: str) -> Sequence[Pattern[str]]: + """Transforms a comma separated list of regular expressions.""" + patterns: List[Pattern[str]] = [] + for pattern in _csv_transformer(value): + patterns.append(re.compile(pattern)) + return patterns + + +_TYPE_TRANSFORMERS: Dict[str, Callable[[str], _ArgumentTypes]] = { "choice": str, - "csv": _csv_validator, + "csv": _csv_transformer, + "int": int, + "non_empty_string": _non_empty_string_transformer, + "regexp": re.compile, + "regexp_csv": _regexp_csv_transfomer, + "string": str, + "yn": _yn_transformer, } -"""Validators for all assignment types.""" +"""Type transformers for all argument types. + +A transformer should accept a string and return one of the supported +Argument types. It will only be called when parsing 1) command-line, +2) configuration files and 3) a string default value. +Non-string default values are assumed to be of the correct type. +""" class _Argument: @@ -52,10 +99,10 @@ def __init__( self.action = action """The action to perform with the argument.""" - self.type = _ASSIGNMENT_VALIDATORS[arg_type] - """A validator function that returns and checks the type of the argument.""" + self.type = _TYPE_TRANSFORMERS[arg_type] + """A transformer function that returns a transformed type of the argument.""" - self.default = self.type(default) + self.default = default """The default value of the argument.""" self.choices = choices diff --git a/pylint/config/arguments_manager.py b/pylint/config/arguments_manager.py index 86aa768e0d..d76afe262c 100644 --- a/pylint/config/arguments_manager.py +++ b/pylint/config/arguments_manager.py @@ -5,7 +5,6 @@ """Arguments manager class used to handle command-line arguments and options.""" import argparse -import configparser from typing import TYPE_CHECKING, Dict, List from pylint.config.argument import _Argument @@ -23,7 +22,7 @@ def __init__(self) -> None: self.namespace = argparse.Namespace() """Namespace for all options.""" - self._arg_parser = argparse.ArgumentParser(prog="pylint") + self._arg_parser = argparse.ArgumentParser(prog="pylint", allow_abbrev=False) """The command line argument parser.""" self._argument_groups_dict: Dict[str, argparse._ArgumentGroup] = {} @@ -75,14 +74,11 @@ def _load_default_argument_values(self) -> None: """Loads the default values of all registered options.""" self.namespace = self._arg_parser.parse_args([], self.namespace) - def _parse_configuration_file( - self, config_parser: configparser.ConfigParser - ) -> None: + def _parse_configuration_file(self, config_data: Dict[str, str]) -> None: """Parse the arguments found in a configuration file into the namespace.""" arguments = [] - for section in config_parser.sections(): - for opt, value in config_parser.items(section): - arguments.extend([f"--{opt}", value]) + for opt, value in config_data.items(): + arguments.extend([f"--{opt}", value]) # pylint: disable-next=fixme # TODO: This should parse_args instead of parse_known_args self.namespace = self._arg_parser.parse_known_args(arguments, self.namespace)[0] diff --git a/pylint/config/config_file_parser.py b/pylint/config/config_file_parser.py new file mode 100644 index 0000000000..dd2faedc4b --- /dev/null +++ b/pylint/config/config_file_parser.py @@ -0,0 +1,92 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt + +"""Configuration file parser class.""" + +import configparser +import os +import sys +from pathlib import Path +from typing import TYPE_CHECKING, Any, Dict, Optional + +if sys.version_info >= (3, 11): + import tomllib +else: + import tomli as tomllib + +if TYPE_CHECKING: + from pylint.lint import PyLinter + + +class _ConfigurationFileParser: + """Class to parse various formats of configuration files.""" + + def __init__(self, verbose: bool, linter: "PyLinter") -> None: + self.verbose_mode = verbose + self.linter = linter + + @staticmethod + def _parse_ini_file(file_path: Path) -> Dict[str, str]: + """Parse and handle errors of a ini configuration file.""" + parser = configparser.ConfigParser(inline_comment_prefixes=("#", ";")) + + # Use this encoding in order to strip the BOM marker, if any. + with open(file_path, encoding="utf_8_sig") as fp: + parser.read_file(fp) + + config_content: Dict[str, str] = {} + for section in parser.sections(): + for opt in parser[section]: + config_content[opt] = parser[section][opt] + return config_content + + @staticmethod + def _parse_toml_value(value: Any) -> str: + """Parse rich toml types into strings.""" + if isinstance(value, (list, tuple)): + return ",".join(value) + return str(value) + + def _parse_toml_file(self, file_path: Path) -> Dict[str, str]: + """Parse and handle errors of a toml configuration file.""" + try: + with open(file_path, mode="rb") as fp: + content = tomllib.load(fp) + except tomllib.TOMLDecodeError as e: + self.linter.add_message("config-parse-error", line=0, args=str(e)) + return {} + + try: + sections_values = content["tool"]["pylint"] + except KeyError: + return {} + + config_content: Dict[str, str] = {} + for opt, values in sections_values.items(): + if isinstance(values, dict): + for config, value in values.items(): + config_content[config] = self._parse_toml_value(value) + else: + config_content[opt] = self._parse_toml_value(values) + return config_content + + def parse_config_file(self, file_path: Optional[Path]) -> Dict[str, str]: + """Parse a config file and return str-str pairs.""" + if file_path is None: + if self.verbose_mode: + print( + "No config file found, using default configuration", file=sys.stderr + ) + return {} + + file_path = Path(os.path.expandvars(file_path)).expanduser() + if not file_path.exists(): + raise OSError(f"The config file {file_path} doesn't exist!") + + if self.verbose_mode: + print(f"Using config file {file_path}", file=sys.stderr) + + if file_path.suffix == ".toml": + return self._parse_toml_file(file_path) + return self._parse_ini_file(file_path) diff --git a/pylint/config/config_initialization.py b/pylint/config/config_initialization.py index 321550da6b..ad982a88ac 100644 --- a/pylint/config/config_initialization.py +++ b/pylint/config/config_initialization.py @@ -6,7 +6,7 @@ from pathlib import Path from typing import TYPE_CHECKING, List, Union -from pylint import reporters +from pylint import config, reporters from pylint.utils import utils if TYPE_CHECKING: @@ -32,6 +32,13 @@ def _config_initialization( linter.set_current_module(config_file) # Read the configuration file + config_file_parser = config._ConfigurationFileParser(verbose_mode, linter) + try: + config_data = config_file_parser.parse_config_file(file_path=config_file_path) + except OSError as ex: + print(ex, file=sys.stderr) + sys.exit(32) + try: # The parser is stored on linter.cfgfile_parser linter.read_config_file(config_file=config_file_path, verbose=verbose_mode) @@ -84,7 +91,7 @@ def _config_initialization( # When finished this should replace the implementation based on optparse # First we parse any options from a configuration file - linter._parse_configuration_file(config_parser) + linter._parse_configuration_file(config_data) # Second we parse any options from the command line, so they can override # the configuration file diff --git a/pylint/config/option_manager_mixin.py b/pylint/config/option_manager_mixin.py index b8e6d25655..ddd75eab44 100644 --- a/pylint/config/option_manager_mixin.py +++ b/pylint/config/option_manager_mixin.py @@ -253,8 +253,8 @@ def read_config_file( if config_file.suffix == ".toml": try: self._parse_toml(config_file, parser) - except tomllib.TOMLDecodeError as e: - self.add_message("config-parse-error", line=0, args=str(e)) # type: ignore[attr-defined] + except tomllib.TOMLDecodeError: + pass else: # Use this encoding in order to strip the BOM marker, if any. with open(config_file, encoding="utf_8_sig") as fp: diff --git a/pylint/reporters/text.py b/pylint/reporters/text.py index d37de5b963..c04c77e3ce 100644 --- a/pylint/reporters/text.py +++ b/pylint/reporters/text.py @@ -108,7 +108,7 @@ def colorize_ansi( def colorize_ansi( msg: str, msg_style: Optional[str] = None, - style: Optional[str] = None, + style: str = "", *, color: Optional[str] = None, ) -> str: @@ -119,7 +119,7 @@ def colorize_ansi( def colorize_ansi( msg: str, msg_style: Union[MessageStyle, str, None] = None, - style: Optional[str] = None, + style: str = "", **kwargs: Optional[str], ) -> str: r"""colorize message by wrapping it with ansi escape codes @@ -267,7 +267,7 @@ def __init__( def __init__( self, output: Optional[TextIO] = None, - color_mapping: Optional[Dict[str, Tuple[Optional[str], Optional[str]]]] = None, + color_mapping: Optional[Dict[str, Tuple[Optional[str], str]]] = None, ) -> None: # Remove for pylint 3.0 ... @@ -276,7 +276,7 @@ def __init__( self, output: Optional[TextIO] = None, color_mapping: Union[ - ColorMappingDict, Dict[str, Tuple[Optional[str], Optional[str]]], None + ColorMappingDict, Dict[str, Tuple[Optional[str], str]], None ] = None, ) -> None: super().__init__(output) @@ -292,7 +292,7 @@ def __init__( temp_color_mapping: ColorMappingDict = {} for key, value in color_mapping.items(): color = value[0] - style_attrs = tuple(_splitstrip(value[1])) + style_attrs = tuple(_splitstrip(value[1])) # type: ignore[arg-type] temp_color_mapping[key] = MessageStyle(color, style_attrs) color_mapping = temp_color_mapping else: diff --git a/pylint/testutils/decorator.py b/pylint/testutils/decorator.py index b2a790c452..85b5b10604 100644 --- a/pylint/testutils/decorator.py +++ b/pylint/testutils/decorator.py @@ -4,8 +4,8 @@ import functools import optparse # pylint: disable=deprecated-module -import warnings +from pylint import config from pylint.lint import PyLinter from pylint.testutils.checker_test_case import CheckerTestCase @@ -44,44 +44,25 @@ def _forward(self, *args, **test_function_kwargs): value, optdict={}, ) - if isinstance(self, CheckerTestCase): - # reopen checker in case, it may be interested in configuration change - self.checker.open() - fun(self, *args, **test_function_kwargs) - - return _forward - - return _wrapper - - -def set_config_directly(**kwargs): - """Decorator for setting config values on a checker without validation. - Some options should be declared in two different checkers. This is - impossible without duplicating the option key. For example: - "no-docstring-rgx" in DocstringParameterChecker & DocStringChecker - This decorator allows to directly set such options. - - Passing the args and kwargs back to the test function itself - allows this decorator to be used on parametrized test cases. - """ - # pylint: disable=fixme - # TODO: Remove this function in 2.14 - warnings.warn( - "The set_config_directly decorator will be removed in 2.14. To decorate " - "unittests you can use set_config. If this causes a duplicate KeyError " - "you can consider writing the tests using the functional test framework.", - DeprecationWarning, - ) - - def _wrapper(fun): - @functools.wraps(fun) - def _forward(self, *args, **test_function_kwargs): + # Set option via argparse + # pylint: disable-next=fixme + # TODO: Revisit this decorator after all checkers have switched + config_file_parser = config._ConfigurationFileParser(False, self.linter) + options = [] for key, value in kwargs.items(): - setattr(self.checker.config, key, value) + options += [ + f"--{key.replace('_', '-')}", + config_file_parser._parse_toml_value(value), + ] + self.linter.namespace = self.linter._arg_parser.parse_known_args( + options, self.linter.namespace + )[0] + if isinstance(self, CheckerTestCase): # reopen checker in case, it may be interested in configuration change self.checker.open() + fun(self, *args, **test_function_kwargs) return _forward diff --git a/pylint/testutils/unittest_linter.py b/pylint/testutils/unittest_linter.py index a7d7fc02df..562a08cf5c 100644 --- a/pylint/testutils/unittest_linter.py +++ b/pylint/testutils/unittest_linter.py @@ -6,13 +6,14 @@ from astroid import nodes +from pylint import config from pylint.interfaces import UNDEFINED, Confidence from pylint.testutils.global_test_linter import linter from pylint.testutils.output_line import MessageTest from pylint.utils import LinterStats -class UnittestLinter: +class UnittestLinter(config._ArgumentsManager): """A fake linter class to capture checker messages.""" # pylint: disable=unused-argument @@ -20,6 +21,7 @@ class UnittestLinter: def __init__(self): self._messages = [] self.stats = LinterStats() + config._ArgumentsManager.__init__(self) def release_messages(self): try: diff --git a/pylint/utils/utils.py b/pylint/utils/utils.py index bf2cb632df..90a082daf4 100644 --- a/pylint/utils/utils.py +++ b/pylint/utils/utils.py @@ -11,6 +11,7 @@ HAS_ISORT_5 = False +import argparse import codecs import os import re @@ -23,6 +24,7 @@ List, Optional, Pattern, + Sequence, TextIO, Tuple, TypeVar, @@ -276,7 +278,7 @@ def get_global_option( return default -def _splitstrip(string, sep=","): +def _splitstrip(string: str, sep: str = ",") -> List[str]: """Return a list of stripped string by splitting the string given as argument on `sep` (',' by default), empty strings are discarded. @@ -314,7 +316,7 @@ def _unquote(string: str) -> str: return string -def _check_csv(value): +def _check_csv(value: Union[List[str], Tuple[str], str]) -> Sequence[str]: if isinstance(value, (list, tuple)): return value return _splitstrip(value) @@ -381,7 +383,7 @@ def _ini_format(stream: TextIO, options: List[Tuple]) -> None: class IsortDriver: """A wrapper around isort API that changed between versions 4 and 5.""" - def __init__(self, config) -> None: + def __init__(self, config: argparse.Namespace) -> None: if HAS_ISORT_5: self.isort5_config = isort.api.Config( # There is no typo here. EXTRA_standard_library is diff --git a/pylintrc b/pylintrc index 7dd03b6867..9a2185c955 100644 --- a/pylintrc +++ b/pylintrc @@ -103,7 +103,7 @@ disable= protected-access, too-few-public-methods, # handled by black - format + format, [REPORTS] diff --git a/tests/functional/d/duplicate_value.py b/tests/functional/d/duplicate_value.py new file mode 100644 index 0000000000..291353c862 --- /dev/null +++ b/tests/functional/d/duplicate_value.py @@ -0,0 +1,18 @@ +# pylint: disable = invalid-name, line-too-long +"""Simple test sets for checking duplicate values""" + +set1 = {1, 2, 3, 4} +set2 = {1, 1, 2} # [duplicate-value] +set3 = {1, 2, 2} # [duplicate-value] + +set4 = {'one', 'two', 'three'} +set5 = {'one', 'two', 'one'} # [duplicate-value] +set6 = {'one', 'two', 'two'} # [duplicate-value] + +wrong_set = {12, 23, True, 6, True, 0, 12} # [duplicate-value, duplicate-value] +correct_set = {12, 13, 23, 24, 89} + +wrong_set_mixed = {1, 2, 'value', 1} # [duplicate-value] +wrong_set = {'arg1', 'arg2', False, 'arg1', True} # [duplicate-value] + +another_wrong_set = {2, 3, 'arg1', True, 'arg1', False, True} # [duplicate-value, duplicate-value] diff --git a/tests/functional/d/duplicate_value.txt b/tests/functional/d/duplicate_value.txt new file mode 100644 index 0000000000..25c154086e --- /dev/null +++ b/tests/functional/d/duplicate_value.txt @@ -0,0 +1,10 @@ +duplicate-value:5:7:5:16::Duplicate value 1 in set:HIGH +duplicate-value:6:7:6:16::Duplicate value 2 in set:HIGH +duplicate-value:9:7:9:28::Duplicate value 'one' in set:HIGH +duplicate-value:10:7:10:28::Duplicate value 'two' in set:HIGH +duplicate-value:12:12:12:42::Duplicate value 12 in set:HIGH +duplicate-value:12:12:12:42::Duplicate value True in set:HIGH +duplicate-value:15:18:15:36::Duplicate value 1 in set:HIGH +duplicate-value:16:12:16:49::Duplicate value 'arg1' in set:HIGH +duplicate-value:18:20:18:61::Duplicate value 'arg1' in set:HIGH +duplicate-value:18:20:18:61::Duplicate value True in set:HIGH diff --git a/tests/functional/n/not_callable.py b/tests/functional/n/not_callable.py index 31d364b885..d0ae5ae3b9 100644 --- a/tests/functional/n/not_callable.py +++ b/tests/functional/n/not_callable.py @@ -200,3 +200,27 @@ def get_number(arg): get_number(10)() # [not-callable] + +class Klass: + def __init__(self): + self._x = None + + @property + def myproperty(self): + if self._x is None: + self._x = lambda: None + return self._x + +myobject = Klass() +myobject.myproperty() + +class Klass2: + @property + def something(self): + if __file__.startswith('s'): + return str + + return 'abcd' + +obj2 = Klass2() +obj2.something() diff --git a/tests/functional/r/redefined/redefined_slots.py b/tests/functional/r/redefined/redefined_slots.py index bd9281e40c..28f05fbfd4 100644 --- a/tests/functional/r/redefined/redefined_slots.py +++ b/tests/functional/r/redefined/redefined_slots.py @@ -1,6 +1,6 @@ """Checks that a subclass does not redefine a slot which has been defined in a parent class.""" -# pylint: disable=too-few-public-methods +# pylint: disable=too-few-public-methods, invalid-slots-object from collections import deque @@ -31,3 +31,9 @@ class Subclass3(Base, Base2): Redefining the `i`, `j`, `k` slot already defined in `Base2` """ __slots__ = ("a", "b", "c", "i", "j", "k", "l", "m", "n") # [redefined-slots-in-subclass] + + +# https://github.com/PyCQA/pylint/issues/6100 +class MyClass: + """No crash when the type of the slot is not a Const or a str""" + __slots__ = [str] diff --git a/tests/functional/u/unnecessary/unnecessary_dunder_call.py b/tests/functional/u/unnecessary/unnecessary_dunder_call.py index 341353a82a..91296296d1 100644 --- a/tests/functional/u/unnecessary/unnecessary_dunder_call.py +++ b/tests/functional/u/unnecessary/unnecessary_dunder_call.py @@ -44,3 +44,14 @@ def __init_subclass__(cls, **kwargs): # Test no lint raised for attributes. my_instance_name = x.__class__.__name__ my_pkg_version = pkg.__version__ + +# Allow use of dunder methods on super() +# since there is no alternate method to call them +class MyClass(list): + def __contains__(self, item): + print("do some special checks") + return super().__contains__(item) + +# But still flag them in other contexts +MY_TEST_BAD = {1, 2, 3}.__contains__(1) # [unnecessary-dunder-call] +MY_TEST_GOOD = 1 in {1, 2, 3} diff --git a/tests/functional/u/unnecessary/unnecessary_dunder_call.txt b/tests/functional/u/unnecessary/unnecessary_dunder_call.txt index 208ad96bb5..9c37170166 100644 --- a/tests/functional/u/unnecessary/unnecessary_dunder_call.txt +++ b/tests/functional/u/unnecessary/unnecessary_dunder_call.txt @@ -1,3 +1,4 @@ unnecessary-dunder-call:6:10:6:28::Unnecessarily calls dunder method __str__. Use str built-in function.:HIGH unnecessary-dunder-call:7:11:7:30::Unnecessarily calls dunder method __add__. Use + operator.:HIGH unnecessary-dunder-call:8:10:8:40::Unnecessarily calls dunder method __repr__. Use repr built-in function.:HIGH +unnecessary-dunder-call:56:14:56:39::Unnecessarily calls dunder method __contains__. Use in keyword.:HIGH diff --git a/tests/functional/u/unnecessary/unnecessary_ellipsis.py b/tests/functional/u/unnecessary/unnecessary_ellipsis.py index 38db9e08e3..e34de754b0 100644 --- a/tests/functional/u/unnecessary/unnecessary_ellipsis.py +++ b/tests/functional/u/unnecessary/unnecessary_ellipsis.py @@ -114,3 +114,18 @@ def func_with_ellipsis_default_arg(a = ...) -> None: # Ignore if the ellipsis is used with a lambda expression print("x", lambda: ...) + + +def func1(val1, _): + if val1 is not ...: + pass + + +def func2(val1, val2): + """Ignore if ellipsis is used on comparisons. + See https://github.com/PyCQA/pylint/issues/6071.""" + if val1 is not ... and val2: + pass + + +assert "x" != ... diff --git a/tests/pyreverse/test_utils.py b/tests/pyreverse/test_utils.py index da75fa9310..70d95346f6 100644 --- a/tests/pyreverse/test_utils.py +++ b/tests/pyreverse/test_utils.py @@ -73,7 +73,7 @@ class A: """ node = astroid.extract_node(assign) instance_attrs = node.instance_attrs - for _, assign_attrs in instance_attrs.items(): + for assign_attrs in instance_attrs.values(): for assign_attr in assign_attrs: got = get_annotation(assign_attr).name assert isinstance(assign_attr, nodes.AssignAttr) diff --git a/tests/test_import_graph.py b/tests/test_import_graph.py index 9ea57e2cef..cfb6613c2d 100644 --- a/tests/test_import_graph.py +++ b/tests/test_import_graph.py @@ -81,6 +81,9 @@ def remove_files() -> Iterator: pass +# pylint: disable-next=fixme +# TODO: Fix these tests after all necessary options support the argparse framework +@pytest.mark.xfail(reason="Not all options support argparse parsing") @pytest.mark.usefixtures("remove_files") def test_checker_dep_graphs(linter: PyLinter) -> None: linter.global_set_option("persistent", False) diff --git a/tests/testutils/test_decorator.py b/tests/testutils/test_decorator.py deleted file mode 100644 index 42b88ab5c9..0000000000 --- a/tests/testutils/test_decorator.py +++ /dev/null @@ -1,15 +0,0 @@ -# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html -# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE -# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt - -import pytest - -from pylint.testutils.decorator import set_config_directly - - -def test_deprecation_of_set_config_directly() -> None: - """Test that the deprecation of set_config_directly works as expected.""" - - with pytest.warns(DeprecationWarning) as records: - set_config_directly() - assert len(records) == 1