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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

70 statements  

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# 

6from __future__ import annotations 

7 

8import argparse 

9import inspect 

10from abc import ABC, abstractmethod 

11from typing import Dict, Generator, List, Tuple, Type, TypeVar 

12 

13from libcst import CSTNode, Module 

14from libcst.codemod._codemod import Codemod 

15from libcst.codemod._context import CodemodContext 

16from libcst.codemod._visitor import ContextAwareTransformer 

17from libcst.codemod.visitors._add_imports import AddImportsVisitor 

18from libcst.codemod.visitors._remove_imports import RemoveImportsVisitor 

19 

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

21 

22 

23class CodemodCommand(Codemod, ABC): 

24 """ 

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

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

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

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

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

30 facilities for automatically running certain common transforms after executing 

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

32 

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

34 

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

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

37 """ 

38 

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

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

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

42 DESCRIPTION: str = "No description." 

43 

44 @staticmethod 

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

46 """ 

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

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

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

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

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

52 class's ``__init__`` method. 

53 """ 

54 

55 pass 

56 

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

58 inst = transform(self.context) 

59 return inst.transform_module(tree) 

60 

61 @abstractmethod 

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

63 """ 

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

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

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

67 """ 

68 ... 

69 

70 # Lightweight wrappers for RemoveImportsVisitor static functions 

71 def remove_unused_import( 

72 self, 

73 module: str, 

74 obj: str | None = None, 

75 asname: str | None = None, 

76 ) -> None: 

77 """ 

78 Schedule an import to be removed after the codemod completes. 

79 

80 This is a convenience wrapper around the :class:`~libcst.codemod.visitors.RemoveImportsVisitor` static function 

81 :meth:`~libcst.codemod.visitors.RemoveImportsVisitor.remove_unused_import` 

82 that automatically passes the codemod context. The import will only be 

83 removed if it is not referenced elsewhere in the module. 

84 

85 For example, to remove ``from typing import Optional``:: 

86 

87 self.remove_unused_import("typing", "Optional") 

88 

89 To remove ``import os``:: 

90 

91 self.remove_unused_import("os") 

92 """ 

93 RemoveImportsVisitor.remove_unused_import(self.context, module, obj, asname) 

94 

95 def remove_unused_import_by_node(self, node: CSTNode) -> None: 

96 """ 

97 Schedule removal of all imports referenced by a node and its children. 

98 

99 This is a convenience wrapper around the :class:`~libcst.codemod.visitors.RemoveImportsVisitor` static function 

100 :meth:`~libcst.codemod.visitors.RemoveImportsVisitor.remove_unused_import_by_node` 

101 that automatically passes the codemod context. This is especially useful 

102 when you are removing a node using :func:`~libcst.RemoveFromParent` and want 

103 to clean up any imports that were only used by that node. 

104 

105 For example:: 

106 

107 def leave_AnnAssign( 

108 self, original_node: cst.AnnAssign, updated_node: cst.AnnAssign, 

109 ) -> cst.RemovalSentinel: 

110 # Remove annotated assignment and clean up imports 

111 self.remove_unused_import_by_node(original_node) 

112 return cst.RemoveFromParent() 

113 

114 Note that you should pass the ``original_node`` rather than ``updated_node`` 

115 since scope analysis is computed on the original tree. 

116 """ 

117 RemoveImportsVisitor.remove_unused_import_by_node(self.context, node) 

118 

119 # Lightweight wrappers for AddImportsVisitor static functions 

120 def add_needed_import( 

121 self, 

122 module: str, 

123 obj: str | None = None, 

124 asname: str | None = None, 

125 relative: int = 0, 

126 ) -> None: 

127 """ 

128 Schedule an import to be added after the codemod completes. 

129 

130 This is a convenience wrapper around the :class:`~libcst.codemod.visitors.AddImportsVisitor` static function 

131 :meth:`~libcst.codemod.visitors.AddImportsVisitor.add_needed_import` 

132 that automatically passes the codemod context. The import will only be 

133 added if it does not already exist in the module. 

134 

135 For example, to add ``from typing import Optional``:: 

136 

137 self.add_needed_import("typing", "Optional") 

138 

139 To add ``import os``:: 

140 

141 self.add_needed_import("os") 

142 

143 To add ``from typing import List as L``:: 

144 

145 self.add_needed_import("typing", "List", asname="L") 

146 """ 

147 AddImportsVisitor.add_needed_import(self.context, module, obj, asname, relative) 

148 

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

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

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

152 tree = super().transform_module(tree) 

153 

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

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

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

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

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

159 supported_transforms: List[Tuple[str, Type[Codemod]]] = [ 

160 (AddImportsVisitor.CONTEXT_KEY, AddImportsVisitor), 

161 (RemoveImportsVisitor.CONTEXT_KEY, RemoveImportsVisitor), 

162 ] 

163 

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

165 for key, transform in supported_transforms: 

166 if key in self.context.scratch: 

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

168 tree = self._instantiate_and_run(transform, tree) 

169 

170 # We're finally done! 

171 return tree 

172 

173 

174class VisitorBasedCodemodCommand(ContextAwareTransformer, CodemodCommand, ABC): 

175 """ 

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

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

178 supported helper transforms after execution. See 

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

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

181 """ 

182 

183 pass 

184 

185 

186class MagicArgsCodemodCommand(CodemodCommand, ABC): 

187 """ 

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

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

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

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

192 constructor arguments that match a key in the context 

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

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

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

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

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

198 scratch for subsequent transforms. 

199 """ 

200 

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

202 super().__init__(context) 

203 self.context.scratch.update(kwargs) 

204 

205 @abstractmethod 

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

207 """ 

208 A generator which yields one or more subclasses of 

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

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

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

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

213 

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

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

216 instantiate them for you with the contents of 

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

218 """ 

219 ... 

220 

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

222 # Grab the expected arguments 

223 argspec = inspect.getfullargspec(transform.__init__) 

224 args: List[object] = [] 

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

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

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

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

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

230 continue 

231 if arg not in self.context.scratch: 

232 if i >= last_default_arg: 

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

234 continue 

235 raise KeyError( 

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

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

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

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

240 ) 

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

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

243 kwonlydefaults = argspec.kwonlydefaults or {} 

244 for kwarg in argspec.kwonlyargs: 

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

246 raise KeyError( 

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

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

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

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

251 ) 

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

253 

254 # Return an instance of the transform with those arguments 

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

256 

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

258 for transform in self.get_transforms(): 

259 inst = self._instantiate(transform) 

260 tree = inst.transform_module(tree) 

261 return tree