deprecated.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  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. """Checker mixin for deprecated functionality."""
  5. from __future__ import annotations
  6. from collections.abc import Container, Iterable
  7. from itertools import chain
  8. import astroid
  9. from astroid import nodes
  10. from pylint.checkers import utils
  11. from pylint.checkers.base_checker import BaseChecker
  12. from pylint.checkers.utils import get_import_name, infer_all, safe_infer
  13. from pylint.typing import MessageDefinitionTuple
  14. ACCEPTABLE_NODES = (
  15. astroid.BoundMethod,
  16. astroid.UnboundMethod,
  17. nodes.FunctionDef,
  18. nodes.ClassDef,
  19. )
  20. class DeprecatedMixin(BaseChecker):
  21. """A mixin implementing logic for checking deprecated symbols.
  22. A class implementing mixin must define "deprecated-method" Message.
  23. """
  24. DEPRECATED_MODULE_MESSAGE: dict[str, MessageDefinitionTuple] = {
  25. "W4901": (
  26. "Deprecated module %r",
  27. "deprecated-module",
  28. "A module marked as deprecated is imported.",
  29. {"old_names": [("W0402", "old-deprecated-module")], "shared": True},
  30. ),
  31. }
  32. DEPRECATED_METHOD_MESSAGE: dict[str, MessageDefinitionTuple] = {
  33. "W4902": (
  34. "Using deprecated method %s()",
  35. "deprecated-method",
  36. "The method is marked as deprecated and will be removed in the future.",
  37. {"old_names": [("W1505", "old-deprecated-method")], "shared": True},
  38. ),
  39. }
  40. DEPRECATED_ARGUMENT_MESSAGE: dict[str, MessageDefinitionTuple] = {
  41. "W4903": (
  42. "Using deprecated argument %s of method %s()",
  43. "deprecated-argument",
  44. "The argument is marked as deprecated and will be removed in the future.",
  45. {"old_names": [("W1511", "old-deprecated-argument")], "shared": True},
  46. ),
  47. }
  48. DEPRECATED_CLASS_MESSAGE: dict[str, MessageDefinitionTuple] = {
  49. "W4904": (
  50. "Using deprecated class %s of module %s",
  51. "deprecated-class",
  52. "The class is marked as deprecated and will be removed in the future.",
  53. {"old_names": [("W1512", "old-deprecated-class")], "shared": True},
  54. ),
  55. }
  56. DEPRECATED_DECORATOR_MESSAGE: dict[str, MessageDefinitionTuple] = {
  57. "W4905": (
  58. "Using deprecated decorator %s()",
  59. "deprecated-decorator",
  60. "The decorator is marked as deprecated and will be removed in the future.",
  61. {"old_names": [("W1513", "old-deprecated-decorator")], "shared": True},
  62. ),
  63. }
  64. @utils.only_required_for_messages(
  65. "deprecated-method",
  66. "deprecated-argument",
  67. "deprecated-class",
  68. )
  69. def visit_call(self, node: nodes.Call) -> None:
  70. """Called when a :class:`nodes.Call` node is visited."""
  71. self.check_deprecated_class_in_call(node)
  72. for inferred in infer_all(node.func):
  73. # Calling entry point for deprecation check logic.
  74. self.check_deprecated_method(node, inferred)
  75. @utils.only_required_for_messages(
  76. "deprecated-module",
  77. "deprecated-class",
  78. )
  79. def visit_import(self, node: nodes.Import) -> None:
  80. """Triggered when an import statement is seen."""
  81. for name in (name for name, _ in node.names):
  82. self.check_deprecated_module(node, name)
  83. if "." in name:
  84. # Checking deprecation for import module with class
  85. mod_name, class_name = name.split(".", 1)
  86. self.check_deprecated_class(node, mod_name, (class_name,))
  87. def deprecated_decorators(self) -> Iterable[str]:
  88. """Callback returning the deprecated decorators.
  89. Returns:
  90. collections.abc.Container of deprecated decorator names.
  91. """
  92. return ()
  93. @utils.only_required_for_messages("deprecated-decorator")
  94. def visit_decorators(self, node: nodes.Decorators) -> None:
  95. """Triggered when a decorator statement is seen."""
  96. children = list(node.get_children())
  97. if not children:
  98. return
  99. if isinstance(children[0], nodes.Call):
  100. inf = safe_infer(children[0].func)
  101. else:
  102. inf = safe_infer(children[0])
  103. qname = inf.qname() if inf else None
  104. if qname in self.deprecated_decorators():
  105. self.add_message("deprecated-decorator", node=node, args=qname)
  106. @utils.only_required_for_messages(
  107. "deprecated-module",
  108. "deprecated-class",
  109. )
  110. def visit_importfrom(self, node: nodes.ImportFrom) -> None:
  111. """Triggered when a from statement is seen."""
  112. basename = node.modname
  113. basename = get_import_name(node, basename)
  114. self.check_deprecated_module(node, basename)
  115. class_names = (name for name, _ in node.names)
  116. self.check_deprecated_class(node, basename, class_names)
  117. def deprecated_methods(self) -> Container[str]:
  118. """Callback returning the deprecated methods/functions.
  119. Returns:
  120. collections.abc.Container of deprecated function/method names.
  121. """
  122. return ()
  123. def deprecated_arguments(self, method: str) -> Iterable[tuple[int | None, str]]:
  124. """Callback returning the deprecated arguments of method/function.
  125. Args:
  126. method (str): name of function/method checked for deprecated arguments
  127. Returns:
  128. collections.abc.Iterable in form:
  129. ((POSITION1, PARAM1), (POSITION2: PARAM2) ...)
  130. where
  131. * POSITIONX - position of deprecated argument PARAMX in function definition.
  132. If argument is keyword-only, POSITIONX should be None.
  133. * PARAMX - name of the deprecated argument.
  134. E.g. suppose function:
  135. .. code-block:: python
  136. def bar(arg1, arg2, arg3, arg4, arg5='spam')
  137. with deprecated arguments `arg2` and `arg4`. `deprecated_arguments` should return:
  138. .. code-block:: python
  139. ((1, 'arg2'), (3, 'arg4'))
  140. """
  141. # pylint: disable=unused-argument
  142. return ()
  143. def deprecated_modules(self) -> Iterable[str]:
  144. """Callback returning the deprecated modules.
  145. Returns:
  146. collections.abc.Container of deprecated module names.
  147. """
  148. return ()
  149. def deprecated_classes(self, module: str) -> Iterable[str]:
  150. """Callback returning the deprecated classes of module.
  151. Args:
  152. module (str): name of module checked for deprecated classes
  153. Returns:
  154. collections.abc.Container of deprecated class names.
  155. """
  156. # pylint: disable=unused-argument
  157. return ()
  158. def check_deprecated_module(self, node: nodes.Import, mod_path: str | None) -> None:
  159. """Checks if the module is deprecated."""
  160. for mod_name in self.deprecated_modules():
  161. if mod_path == mod_name or mod_path and mod_path.startswith(mod_name + "."):
  162. self.add_message("deprecated-module", node=node, args=mod_path)
  163. def check_deprecated_method(self, node: nodes.Call, inferred: nodes.NodeNG) -> None:
  164. """Executes the checker for the given node.
  165. This method should be called from the checker implementing this mixin.
  166. """
  167. # Reject nodes which aren't of interest to us.
  168. if not isinstance(inferred, ACCEPTABLE_NODES):
  169. return
  170. if isinstance(node.func, nodes.Attribute):
  171. func_name = node.func.attrname
  172. elif isinstance(node.func, nodes.Name):
  173. func_name = node.func.name
  174. else:
  175. # Not interested in other nodes.
  176. return
  177. qnames = {inferred.qname(), func_name}
  178. if any(name in self.deprecated_methods() for name in qnames):
  179. self.add_message("deprecated-method", node=node, args=(func_name,))
  180. return
  181. num_of_args = len(node.args)
  182. kwargs = {kw.arg for kw in node.keywords} if node.keywords else {}
  183. deprecated_arguments = (self.deprecated_arguments(qn) for qn in qnames)
  184. for position, arg_name in chain(*deprecated_arguments):
  185. if arg_name in kwargs:
  186. # function was called with deprecated argument as keyword argument
  187. self.add_message(
  188. "deprecated-argument", node=node, args=(arg_name, func_name)
  189. )
  190. elif position is not None and position < num_of_args:
  191. # function was called with deprecated argument as positional argument
  192. self.add_message(
  193. "deprecated-argument", node=node, args=(arg_name, func_name)
  194. )
  195. def check_deprecated_class(
  196. self, node: nodes.NodeNG, mod_name: str, class_names: Iterable[str]
  197. ) -> None:
  198. """Checks if the class is deprecated."""
  199. for class_name in class_names:
  200. if class_name in self.deprecated_classes(mod_name):
  201. self.add_message(
  202. "deprecated-class", node=node, args=(class_name, mod_name)
  203. )
  204. def check_deprecated_class_in_call(self, node: nodes.Call) -> None:
  205. """Checks if call the deprecated class."""
  206. if isinstance(node.func, nodes.Attribute) and isinstance(
  207. node.func.expr, nodes.Name
  208. ):
  209. mod_name = node.func.expr.name
  210. class_name = node.func.attrname
  211. self.check_deprecated_class(node, mod_name, (class_name,))