method_args.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  2. # For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE
  3. # Copyright (c) https://github.com/pylint-dev/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. ),
  35. }
  36. options = (
  37. (
  38. "timeout-methods",
  39. {
  40. "default": (
  41. "requests.api.delete",
  42. "requests.api.get",
  43. "requests.api.head",
  44. "requests.api.options",
  45. "requests.api.patch",
  46. "requests.api.post",
  47. "requests.api.put",
  48. "requests.api.request",
  49. ),
  50. "type": "csv",
  51. "metavar": "<comma separated list>",
  52. "help": "List of qualified names (i.e., library.method) which require a timeout parameter "
  53. "e.g. 'requests.api.get,requests.api.post'",
  54. },
  55. ),
  56. )
  57. @utils.only_required_for_messages(
  58. "missing-timeout", "positional-only-arguments-expected"
  59. )
  60. def visit_call(self, node: nodes.Call) -> None:
  61. self._check_missing_timeout(node)
  62. self._check_positional_only_arguments_expected(node)
  63. def _check_missing_timeout(self, node: nodes.Call) -> None:
  64. """Check if the call needs a timeout parameter based on package.func_name
  65. configured in config.timeout_methods.
  66. Package uses inferred node in order to know the package imported.
  67. """
  68. inferred = utils.safe_infer(node.func)
  69. call_site = arguments.CallSite.from_call(node)
  70. if (
  71. inferred
  72. and not call_site.has_invalid_keywords()
  73. and isinstance(
  74. inferred, (nodes.FunctionDef, nodes.ClassDef, bases.UnboundMethod)
  75. )
  76. and inferred.qname() in self.linter.config.timeout_methods
  77. ):
  78. keyword_arguments = [keyword.arg for keyword in node.keywords]
  79. keyword_arguments.extend(call_site.keyword_arguments)
  80. if "timeout" not in keyword_arguments:
  81. self.add_message(
  82. "missing-timeout",
  83. node=node,
  84. args=(node.func.as_string(),),
  85. confidence=INFERENCE,
  86. )
  87. def _check_positional_only_arguments_expected(self, node: nodes.Call) -> None:
  88. """Check if positional only arguments have been passed as keyword arguments by
  89. inspecting its method definition.
  90. """
  91. inferred_func = utils.safe_infer(node.func)
  92. while isinstance(inferred_func, (astroid.BoundMethod, astroid.UnboundMethod)):
  93. inferred_func = inferred_func._proxied
  94. if not (
  95. isinstance(inferred_func, (nodes.FunctionDef))
  96. and inferred_func.args.posonlyargs
  97. ):
  98. return
  99. if inferred_func.args.kwarg:
  100. return
  101. pos_args = [a.name for a in inferred_func.args.posonlyargs]
  102. kws = [k.arg for k in node.keywords if k.arg in pos_args]
  103. if not kws:
  104. return
  105. self.add_message(
  106. "positional-only-arguments-expected",
  107. node=node,
  108. args=(node.func.as_string(), ", ".join(f"'{k}'" for k in kws)),
  109. confidence=INFERENCE,
  110. )
  111. def register(linter: PyLinter) -> None:
  112. linter.register_checker(MethodArgsChecker(linter))