1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
|
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
#
"""
Provides everything needed to run a CodemodCommand.
"""
import traceback
from dataclasses import dataclass
from enum import Enum
from typing import Optional, Sequence, Union
from libcst import parse_module, PartialParserConfig
from libcst.codemod._codemod import Codemod
# All datastructures defined in this class are pickleable so that they can be used
# as a return value with the multiprocessing module.
@dataclass(frozen=True)
class TransformSuccess:
"""
A :class:`~libcst.codemod.TransformResult` used when the codemod was successful.
Stores all the information we might need to display to the user upon success, as
well as the transformed file contents.
"""
#: All warning messages that were generated during the codemod.
warning_messages: Sequence[str]
#: The updated code, post-codemod.
code: str
@dataclass(frozen=True)
class TransformFailure:
"""
A :class:`~libcst.codemod.TransformResult` used when the codemod failed.
Stores all the information we might need to display to the user upon a failure.
"""
#: All warning messages that were generated before the codemod crashed.
warning_messages: Sequence[str]
#: The exception that was raised during the codemod.
error: Exception
#: The traceback string that was recorded at the time of exception.
traceback_str: str
@dataclass(frozen=True)
class TransformExit:
"""
A :class:`~libcst.codemod.TransformResult` used when the codemod was interrupted
by the user (e.g. KeyboardInterrupt).
"""
#: An empty list of warnings, included so that all
#: :class:`~libcst.codemod.TransformResult` have a ``warning_messages`` attribute.
warning_messages: Sequence[str] = ()
class SkipReason(Enum):
"""
An enumeration of all valid reasons for a codemod to skip.
"""
#: The module was skipped because we detected that it was generated code, and
#: we were configured to skip generated files.
GENERATED = "generated"
#: The module was skipped because we detected that it was blacklisted, and we
#: were configured to skip blacklisted files.
BLACKLISTED = "blacklisted"
#: The module was skipped because the codemod requested us to skip using the
#: :class:`~libcst.codemod.SkipFile` exception.
OTHER = "other"
@dataclass(frozen=True)
class TransformSkip:
"""
A :class:`~libcst.codemod.TransformResult` used when the codemod requested to
be skipped. This could be because it's a generated file, or due to filename
blacklist, or because the transform raised :class:`~libcst.codemod.SkipFile`.
"""
#: The reason that we skipped codemodding this module.
skip_reason: SkipReason
#: The description populated from the :class:`~libcst.codemod.SkipFile` exception.
skip_description: str
#: All warning messages that were generated before the codemod decided to skip.
warning_messages: Sequence[str] = ()
class SkipFile(Exception):
"""
Raise this exception to skip codemodding the current file.
The exception message should be the reason for skipping.
"""
TransformResult = Union[
TransformSuccess, TransformFailure, TransformExit, TransformSkip
]
def transform_module(
transformer: Codemod, code: str, *, python_version: Optional[str] = None
) -> TransformResult:
"""
Given a module as represented by a string and a :class:`~libcst.codemod.Codemod`
that we wish to run, execute the codemod on the code and return a
:class:`~libcst.codemod.TransformResult`. This should never raise an exception.
On success, this returns a :class:`~libcst.codemod.TransformSuccess` containing
any generated warnings as well as the transformed code. If the codemod is
interrupted with a Ctrl+C, this returns a :class:`~libcst.codemod.TransformExit`.
If the codemod elected to skip by throwing a :class:`~libcst.codemod.SkipFile`
exception, this will return a :class:`~libcst.codemod.TransformSkip` containing
the reason for skipping as well as any warnings that were generated before
the codemod decided to skip. If the codemod throws an unexpected exception,
this will return a :class:`~libcst.codemod.TransformFailure` containing the
exception that occured as well as any warnings that were generated before the
codemod crashed.
"""
try:
input_tree = parse_module(
code,
config=(
PartialParserConfig(python_version=python_version)
if python_version is not None
else PartialParserConfig()
),
)
output_tree = transformer.transform_module(input_tree)
return TransformSuccess(
code=output_tree.code, warning_messages=transformer.context.warnings
)
except KeyboardInterrupt:
return TransformExit()
except SkipFile as ex:
return TransformSkip(
skip_description=str(ex),
skip_reason=SkipReason.OTHER,
warning_messages=transformer.context.warnings,
)
except Exception as ex:
return TransformFailure(
error=ex,
traceback_str=traceback.format_exc(),
warning_messages=transformer.context.warnings,
)
|