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 RemoveImportsVisitor.remove_unused_import(self.context, module, obj, asname) 

78 

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

80 RemoveImportsVisitor.remove_unused_import_by_node(self.context, node) 

81 

82 # Lightweight wrappers for AddImportsVisitor static functions 

83 def add_needed_import( 

84 self, 

85 module: str, 

86 obj: str | None = None, 

87 asname: str | None = None, 

88 relative: int = 0, 

89 ) -> None: 

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

91 

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

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

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

95 tree = super().transform_module(tree) 

96 

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

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

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

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

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

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

103 (AddImportsVisitor.CONTEXT_KEY, AddImportsVisitor), 

104 (RemoveImportsVisitor.CONTEXT_KEY, RemoveImportsVisitor), 

105 ] 

106 

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

108 for key, transform in supported_transforms: 

109 if key in self.context.scratch: 

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

111 tree = self._instantiate_and_run(transform, tree) 

112 

113 # We're finally done! 

114 return tree 

115 

116 

117class VisitorBasedCodemodCommand(ContextAwareTransformer, CodemodCommand, ABC): 

118 """ 

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

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

121 supported helper transforms after execution. See 

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

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

124 """ 

125 

126 pass 

127 

128 

129class MagicArgsCodemodCommand(CodemodCommand, ABC): 

130 """ 

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

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

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

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

135 constructor arguments that match a key in the context 

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

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

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

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

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

141 scratch for subsequent transforms. 

142 """ 

143 

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

145 super().__init__(context) 

146 self.context.scratch.update(kwargs) 

147 

148 @abstractmethod 

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

150 """ 

151 A generator which yields one or more subclasses of 

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

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

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

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

156 

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

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

159 instantiate them for you with the contents of 

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

161 """ 

162 ... 

163 

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

165 # Grab the expected arguments 

166 argspec = inspect.getfullargspec(transform.__init__) 

167 args: List[object] = [] 

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

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

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

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

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

173 continue 

174 if arg not in self.context.scratch: 

175 if i >= last_default_arg: 

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

177 continue 

178 raise KeyError( 

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

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

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

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

183 ) 

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

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

186 kwonlydefaults = argspec.kwonlydefaults or {} 

187 for kwarg in argspec.kwonlyargs: 

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

189 raise KeyError( 

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

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

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

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

194 ) 

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

196 

197 # Return an instance of the transform with those arguments 

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

199 

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

201 for transform in self.get_transforms(): 

202 inst = self._instantiate(transform) 

203 tree = inst.transform_module(tree) 

204 return tree