method_args.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  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. """Variables checkers for Python code."""
  5. from __future__ import annotations
  6. from typing import TYPE_CHECKING
  7. import astroid
  8. from astroid import arguments, bases, nodes
  9. from pylint.checkers import BaseChecker, utils
  10. from pylint.interfaces import INFERENCE
  11. if TYPE_CHECKING:
  12. from pylint.lint import PyLinter
  13. class MethodArgsChecker(BaseChecker):
  14. """BaseChecker for method_args.
  15. Checks for
  16. * missing-timeout
  17. * positional-only-arguments-expected
  18. """
  19. name = "method_args"
  20. msgs = {
  21. "W3101": (
  22. "Missing timeout argument for method '%s' can cause your program to hang indefinitely",
  23. "missing-timeout",
  24. "Used when a method needs a 'timeout' parameter in order to avoid waiting "
  25. "for a long time. If no timeout is specified explicitly the default value "
  26. "is used. For example for 'requests' the program will never time out "
  27. "(i.e. hang indefinitely).",
  28. ),
  29. "E3102": (
  30. "`%s()` got some positional-only arguments passed as keyword arguments: %s",
  31. "positional-only-arguments-expected",
  32. "Emitted when positional-only arguments have been passed as keyword arguments. "
  33. "Remove the keywords for the affected arguments in the function call.",
  34. {"minversion": (3, 8)},
  35. ),
  36. }
  37. options = (
  38. (
  39. "timeout-methods",
  40. {
  41. "default": (
  42. "requests.api.delete",
  43. "requests.api.get",
  44. "requests.api.head",
  45. "requests.api.options",
  46. "requests.api.patch",
  47. "requests.api.post",
  48. "requests.api.put",
  49. "requests.api.request",
  50. ),
  51. "type": "csv",
  52. "metavar": "<comma separated list>",
  53. "help": "List of qualified names (i.e., library.method) which require a timeout parameter "
  54. "e.g. 'requests.api.get,requests.api.post'",
  55. },
  56. ),
  57. )
  58. @utils.only_required_for_messages(
  59. "missing-timeout", "positional-only-arguments-expected"
  60. )
  61. def visit_call(self, node: nodes.Call) -> None:
  62. self._check_missing_timeout(node)
  63. self._check_positional_only_arguments_expected(node)
  64. def _check_missing_timeout(self, node: nodes.Call) -> None:
  65. """Check if the call needs a timeout parameter based on package.func_name
  66. configured in config.timeout_methods.
  67. Package uses inferred node in order to know the package imported.
  68. """
  69. inferred = utils.safe_infer(node.func)
  70. call_site = arguments.CallSite.from_call(node)
  71. if (
  72. inferred
  73. and not call_site.has_invalid_keywords()
  74. and isinstance(
  75. inferred, (nodes.FunctionDef, nodes.ClassDef, bases.UnboundMethod)
  76. )
  77. and inferred.qname() in self.linter.config.timeout_methods
  78. ):
  79. keyword_arguments = [keyword.arg for keyword in node.keywords]
  80. keyword_arguments.extend(call_site.keyword_arguments)
  81. if "timeout" not in keyword_arguments:
  82. self.add_message(
  83. "missing-timeout",
  84. node=node,
  85. args=(node.func.as_string(),),
  86. confidence=INFERENCE,
  87. )
  88. def _check_positional_only_arguments_expected(self, node: nodes.Call) -> None:
  89. """Check if positional only arguments have been passed as keyword arguments by
  90. inspecting its method definition.
  91. """
  92. inferred_func = utils.safe_infer(node.func)
  93. while isinstance(inferred_func, (astroid.BoundMethod, astroid.UnboundMethod)):
  94. inferred_func = inferred_func._proxied
  95. if not (
  96. isinstance(inferred_func, (nodes.FunctionDef))
  97. and inferred_func.args.posonlyargs
  98. ):
  99. return
  100. if inferred_func.args.kwarg:
  101. return
  102. pos_args = [a.name for a in inferred_func.args.posonlyargs]
  103. kws = [k.arg for k in node.keywords if k.arg in pos_args]
  104. if not kws:
  105. return
  106. self.add_message(
  107. "positional-only-arguments-expected",
  108. node=node,
  109. args=(node.func.as_string(), ", ".join(f"'{k}'" for k in kws)),
  110. confidence=INFERENCE,
  111. )
  112. def register(linter: PyLinter) -> None:
  113. linter.register_checker(MethodArgsChecker(linter))