Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/libcst/codemod/_command.py: 52%

62 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-09-25 06:43 +0000

1# Copyright (c) Meta Platforms, Inc. and affiliates. 

2# 

3# This source code is licensed under the MIT license found in the 

4# LICENSE file in the root directory of this source tree. 

5# 

6import argparse 

7import inspect 

8from abc import ABC, abstractmethod 

9from typing import Dict, Generator, List, Type, TypeVar 

10 

11from libcst import Module 

12from libcst.codemod._codemod import Codemod 

13from libcst.codemod._context import CodemodContext 

14from libcst.codemod._visitor import ContextAwareTransformer 

15from libcst.codemod.visitors._add_imports import AddImportsVisitor 

16from libcst.codemod.visitors._remove_imports import RemoveImportsVisitor 

17 

18_Codemod = TypeVar("_Codemod", bound=Codemod) 

19 

20 

21class CodemodCommand(Codemod, ABC): 

22 """ 

23 A :class:`~libcst.codemod.Codemod` which can be invoked on the command-line 

24 using the ``libcst.tool codemod`` utility. It behaves like any other codemod 

25 in that it can be instantiated and run identically to a 

26 :class:`~libcst.codemod.Codemod`. However, it provides support for providing 

27 help text and command-line arguments to ``libcst.tool codemod`` as well as 

28 facilities for automatically running certain common transforms after executing 

29 your :meth:`~libcst.codemod.Codemod.transform_module_impl`. 

30 

31 The following list of transforms are automatically run at this time: 

32 

33 - :class:`~libcst.codemod.visitors.AddImportsVisitor` (adds needed imports to a module). 

34 - :class:`~libcst.codemod.visitors.RemoveImportsVisitor` (removes unreferenced imports from a module). 

35 """ 

36 

37 #: An overrideable description attribute so that codemods can provide 

38 #: a short summary of what they do. This description will show up in 

39 #: command-line help as well as when listing available codemods. 

40 DESCRIPTION: str = "No description." 

41 

42 @staticmethod 

43 def add_args(arg_parser: argparse.ArgumentParser) -> None: 

44 """ 

45 Override this to add arguments to the CLI argument parser. These args 

46 will show up when the user invokes ``libcst.tool codemod`` with 

47 ``--help``. They will also be presented to your class's ``__init__`` 

48 method. So, if you define a command with an argument 'foo', you should also 

49 have a corresponding 'foo' positional or keyword argument in your 

50 class's ``__init__`` method. 

51 """ 

52 

53 pass 

54 

55 def _instantiate_and_run(self, transform: Type[_Codemod], tree: Module) -> Module: 

56 inst = transform(self.context) 

57 return inst.transform_module(tree) 

58 

59 @abstractmethod 

60 def transform_module_impl(self, tree: Module) -> Module: 

61 """ 

62 Override this with your transform. You should take in the tree, optionally 

63 mutate it and then return the mutated version. The module reference and all 

64 calculated metadata are available for the lifetime of this function. 

65 """ 

66 ... 

67 

68 def transform_module(self, tree: Module) -> Module: 

69 # Overrides (but then calls) Codemod's transform_module to provide 

70 # a spot where additional supported transforms can be attached and run. 

71 tree = super().transform_module(tree) 

72 

73 # List of transforms we should run, with their context key they use 

74 # for storing in context.scratch. Typically, the transform will also 

75 # have a static method that other transforms can use which takes 

76 # a context and other optional args and modifies its own context key 

77 # accordingly. We import them here so that we don't have circular imports. 

78 supported_transforms: Dict[str, Type[Codemod]] = { 

79 AddImportsVisitor.CONTEXT_KEY: AddImportsVisitor, 

80 RemoveImportsVisitor.CONTEXT_KEY: RemoveImportsVisitor, 

81 } 

82 

83 # For any visitors that we support auto-running, run them here if needed. 

84 for key, transform in supported_transforms.items(): 

85 if key in self.context.scratch: 

86 # We have work to do, so lets run this. 

87 tree = self._instantiate_and_run(transform, tree) 

88 

89 # We're finally done! 

90 return tree 

91 

92 

93class VisitorBasedCodemodCommand(ContextAwareTransformer, CodemodCommand, ABC): 

94 """ 

95 A command that acts identically to a visitor-based transform, but also has 

96 the support of :meth:`~libcst.codemod.CodemodCommand.add_args` and running 

97 supported helper transforms after execution. See 

98 :class:`~libcst.codemod.CodemodCommand` and 

99 :class:`~libcst.codemod.ContextAwareTransformer` for additional documentation. 

100 """ 

101 

102 pass 

103 

104 

105class MagicArgsCodemodCommand(CodemodCommand, ABC): 

106 """ 

107 A "magic" args command, which auto-magically looks up the transforms that 

108 are yielded from :meth:`~libcst.codemod.MagicArgsCodemodCommand.get_transforms` 

109 and instantiates them using values out of the context. Visitors yielded in 

110 :meth:`~libcst.codemod.MagicArgsCodemodCommand.get_transforms` must have 

111 constructor arguments that match a key in the context 

112 :attr:`~libcst.codemod.CodemodContext.scratch`. The easiest way to 

113 guarantee that is to use :meth:`~libcst.codemod.CodemodCommand.add_args` 

114 to add a command arg that will be parsed for each of the args. However, if 

115 you wish to chain transforms, adding to the scratch in one transform will make 

116 the value available to the constructor in subsequent transforms as well as the 

117 scratch for subsequent transforms. 

118 """ 

119 

120 def __init__(self, context: CodemodContext, **kwargs: Dict[str, object]) -> None: 

121 super().__init__(context) 

122 self.context.scratch.update(kwargs) 

123 

124 @abstractmethod 

125 def get_transforms(self) -> Generator[Type[Codemod], None, None]: 

126 """ 

127 A generator which yields one or more subclasses of 

128 :class:`~libcst.codemod.Codemod`. In the general case, you will usually 

129 yield a series of classes, but it is possible to programmatically decide 

130 which classes to yield depending on the contents of the context 

131 :attr:`~libcst.codemod.CodemodContext.scratch`. 

132 

133 Note that you should yield classes, not instances of classes, as the 

134 point of :class:`~libcst.codemod.MagicArgsCodemodCommand` is to 

135 instantiate them for you with the contents of 

136 :attr:`~libcst.codemod.CodemodContext.scratch`. 

137 """ 

138 ... 

139 

140 def _instantiate(self, transform: Type[_Codemod]) -> _Codemod: 

141 # Grab the expected arguments 

142 argspec = inspect.getfullargspec(transform.__init__) 

143 args: List[object] = [] 

144 kwargs: Dict[str, object] = {} 

145 last_default_arg = len(argspec.args) - len(argspec.defaults or ()) 

146 for i, arg in enumerate(argspec.args): 

147 if arg in ["self", "context"]: 

148 # Self is bound, and context we explicitly include below. 

149 continue 

150 if arg not in self.context.scratch: 

151 if i >= last_default_arg: 

152 # This arg has a default, so the fact that its missing is fine. 

153 continue 

154 raise KeyError( 

155 f"Visitor {transform.__name__} requires positional arg {arg} but " 

156 + "it is not in our context nor does it have a default! It should " 

157 + "be provided by an argument returned from the 'add_args' method " 

158 + "or populated into context.scratch by a previous transform!" 

159 ) 

160 # No default, but we found something in scratch. So, forward it. 

161 args.append(self.context.scratch[arg]) 

162 kwonlydefaults = argspec.kwonlydefaults or {} 

163 for kwarg in argspec.kwonlyargs: 

164 if kwarg not in self.context.scratch and kwarg not in kwonlydefaults: 

165 raise KeyError( 

166 f"Visitor {transform.__name__} requires keyword arg {kwarg} but " 

167 + "it is not in our context nor does it have a default! It should " 

168 + "be provided by an argument returned from the 'add_args' method " 

169 + "or populated into context.scratch by a previous transform!" 

170 ) 

171 kwargs[kwarg] = self.context.scratch.get(kwarg, kwonlydefaults[kwarg]) 

172 

173 # Return an instance of the transform with those arguments 

174 return transform(self.context, *args, **kwargs) 

175 

176 def transform_module_impl(self, tree: Module) -> Module: 

177 for transform in self.get_transforms(): 

178 inst = self._instantiate(transform) 

179 tree = inst.transform_module(tree) 

180 return tree