dunder_methods.py 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  2. # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
  3. # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt
  4. from __future__ import annotations
  5. from typing import TYPE_CHECKING
  6. from astroid import Instance, nodes
  7. from astroid.util import UninferableBase
  8. from pylint.checkers import BaseChecker
  9. from pylint.checkers.utils import safe_infer
  10. from pylint.constants import DUNDER_METHODS
  11. from pylint.interfaces import HIGH
  12. if TYPE_CHECKING:
  13. from pylint.lint import PyLinter
  14. class DunderCallChecker(BaseChecker):
  15. """Check for unnecessary dunder method calls.
  16. Docs: https://docs.python.org/3/reference/datamodel.html#basic-customization
  17. We exclude names in list pylint.constants.EXTRA_DUNDER_METHODS such as
  18. __index__ (see https://github.com/PyCQA/pylint/issues/6795)
  19. since these either have no alternative method of being called or
  20. have a genuine use case for being called manually.
  21. Additionally, we exclude classes that are not instantiated since these
  22. might be used to access the dunder methods of a base class of an instance.
  23. We also exclude dunder method calls on super() since
  24. these can't be written in an alternative manner.
  25. """
  26. name = "unnecessary-dunder-call"
  27. priority = -1
  28. msgs = {
  29. "C2801": (
  30. "Unnecessarily calls dunder method %s. %s.",
  31. "unnecessary-dunder-call",
  32. "Used when a dunder method is manually called instead "
  33. "of using the corresponding function/method/operator.",
  34. ),
  35. }
  36. options = ()
  37. def open(self) -> None:
  38. self._dunder_methods: dict[str, str] = {}
  39. for since_vers, dunder_methods in DUNDER_METHODS.items():
  40. if since_vers <= self.linter.config.py_version:
  41. self._dunder_methods.update(dunder_methods)
  42. @staticmethod
  43. def within_dunder_def(node: nodes.NodeNG) -> bool:
  44. """Check if dunder method call is within a dunder method definition."""
  45. parent = node.parent
  46. while parent is not None:
  47. if (
  48. isinstance(parent, nodes.FunctionDef)
  49. and parent.name.startswith("__")
  50. and parent.name.endswith("__")
  51. ):
  52. return True
  53. parent = parent.parent
  54. return False
  55. def visit_call(self, node: nodes.Call) -> None:
  56. """Check if method being called is an unnecessary dunder method."""
  57. if (
  58. isinstance(node.func, nodes.Attribute)
  59. and node.func.attrname in self._dunder_methods
  60. and not self.within_dunder_def(node)
  61. and not (
  62. isinstance(node.func.expr, nodes.Call)
  63. and isinstance(node.func.expr.func, nodes.Name)
  64. and node.func.expr.func.name == "super"
  65. )
  66. ):
  67. inf_expr = safe_infer(node.func.expr)
  68. if not (
  69. inf_expr is None or isinstance(inf_expr, (Instance, UninferableBase))
  70. ):
  71. # Skip dunder calls to non instantiated classes.
  72. return
  73. self.add_message(
  74. "unnecessary-dunder-call",
  75. node=node,
  76. args=(node.func.attrname, self._dunder_methods[node.func.attrname]),
  77. confidence=HIGH,
  78. )
  79. def register(linter: PyLinter) -> None:
  80. linter.register_checker(DunderCallChecker(linter))