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
« 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
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
18_Codemod = TypeVar("_Codemod", bound=Codemod)
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`.
31 The following list of transforms are automatically run at this time:
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 """
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."
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 """
53 pass
55 def _instantiate_and_run(self, transform: Type[_Codemod], tree: Module) -> Module:
56 inst = transform(self.context)
57 return inst.transform_module(tree)
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 ...
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)
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 }
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)
89 # We're finally done!
90 return tree
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 """
102 pass
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 """
120 def __init__(self, context: CodemodContext, **kwargs: Dict[str, object]) -> None:
121 super().__init__(context)
122 self.context.scratch.update(kwargs)
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`.
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 ...
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])
173 # Return an instance of the transform with those arguments
174 return transform(self.context, *args, **kwargs)
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