Statistics
| Branch: | Tag: | Revision:

root / snf-deploy / snfdeploy / massedit.py @ 7aa13555

History | View | Annotate | Download (10.6 kB)

1 ad76b118 Dimitris Aragiorgis
#!/usr/bin/env python
2 ad76b118 Dimitris Aragiorgis
# encoding='cp1252'
3 ad76b118 Dimitris Aragiorgis
4 ad76b118 Dimitris Aragiorgis
"""A python bulk editor class to apply the same code to many files."""
5 ad76b118 Dimitris Aragiorgis
6 ad76b118 Dimitris Aragiorgis
# Copyright (c) 2012 Jerome Lecomte
7 ad76b118 Dimitris Aragiorgis
#
8 ad76b118 Dimitris Aragiorgis
# Permission is hereby granted, free of charge, to any person obtaining a copy
9 ad76b118 Dimitris Aragiorgis
# of this software and associated documentation files (the "Software"), to deal
10 ad76b118 Dimitris Aragiorgis
# in the Software without restriction, including without limitation the rights
11 ad76b118 Dimitris Aragiorgis
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 ad76b118 Dimitris Aragiorgis
# copies of the Software, and to permit persons to whom the Software is
13 ad76b118 Dimitris Aragiorgis
# furnished to do so, subject to the following conditions:
14 ad76b118 Dimitris Aragiorgis
#
15 ad76b118 Dimitris Aragiorgis
# The above copyright notice and this permission notice shall be included in
16 ad76b118 Dimitris Aragiorgis
# all copies or substantial portions of the Software.
17 ad76b118 Dimitris Aragiorgis
#
18 ad76b118 Dimitris Aragiorgis
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 ad76b118 Dimitris Aragiorgis
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 ad76b118 Dimitris Aragiorgis
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 ad76b118 Dimitris Aragiorgis
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 ad76b118 Dimitris Aragiorgis
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 ad76b118 Dimitris Aragiorgis
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 ad76b118 Dimitris Aragiorgis
# THE SOFTWARE.
25 ad76b118 Dimitris Aragiorgis
26 ad76b118 Dimitris Aragiorgis
27 ad76b118 Dimitris Aragiorgis
__version__ = '0.61'  # UPDATE setup.py when changing version.
28 ad76b118 Dimitris Aragiorgis
__author__ = 'Jerome Lecomte'
29 ad76b118 Dimitris Aragiorgis
__license__ = 'MIT'
30 ad76b118 Dimitris Aragiorgis
31 ad76b118 Dimitris Aragiorgis
32 ad76b118 Dimitris Aragiorgis
import os
33 ad76b118 Dimitris Aragiorgis
import sys
34 ad76b118 Dimitris Aragiorgis
import logging
35 ad76b118 Dimitris Aragiorgis
import argparse
36 ad76b118 Dimitris Aragiorgis
import difflib
37 ad76b118 Dimitris Aragiorgis
# Most manip will involve re so we include it here for convenience.
38 ad76b118 Dimitris Aragiorgis
import re  # pylint: disable=W0611
39 ad76b118 Dimitris Aragiorgis
import fnmatch
40 ad76b118 Dimitris Aragiorgis
41 ad76b118 Dimitris Aragiorgis
42 ad76b118 Dimitris Aragiorgis
logger = logging.getLogger(__name__)
43 ad76b118 Dimitris Aragiorgis
44 ad76b118 Dimitris Aragiorgis
45 ad76b118 Dimitris Aragiorgis
class EditorError(RuntimeError):
46 ad76b118 Dimitris Aragiorgis
    """Error raised by the Editor class."""
47 ad76b118 Dimitris Aragiorgis
    pass
48 ad76b118 Dimitris Aragiorgis
49 ad76b118 Dimitris Aragiorgis
50 ad76b118 Dimitris Aragiorgis
class Editor(object):
51 ad76b118 Dimitris Aragiorgis
    """Processes input file or input line.
52 ad76b118 Dimitris Aragiorgis

53 ad76b118 Dimitris Aragiorgis
    Named arguments:
54 ad76b118 Dimitris Aragiorgis
    code -- code expression to process the input with.
55 ad76b118 Dimitris Aragiorgis
    """
56 ad76b118 Dimitris Aragiorgis
57 ad76b118 Dimitris Aragiorgis
    def __init__(self, **kwds):
58 ad76b118 Dimitris Aragiorgis
        self.code_objs = dict()
59 ad76b118 Dimitris Aragiorgis
        self._codes = []
60 ad76b118 Dimitris Aragiorgis
        self.dry_run = None
61 ad76b118 Dimitris Aragiorgis
        if 'module' in kwds:
62 ad76b118 Dimitris Aragiorgis
            self.import_module(kwds['module'])
63 ad76b118 Dimitris Aragiorgis
        if 'code' in kwds:
64 ad76b118 Dimitris Aragiorgis
            self.append_code_expr(kwds['code'])
65 ad76b118 Dimitris Aragiorgis
        if 'dry_run' in kwds:
66 ad76b118 Dimitris Aragiorgis
            self.dry_run = kwds['dry_run']
67 ad76b118 Dimitris Aragiorgis
68 ad76b118 Dimitris Aragiorgis
    def __edit_line(self, line, code, code_obj):  # pylint: disable=R0201
69 ad76b118 Dimitris Aragiorgis
        """Edit a line with one code object built in the ctor."""
70 ad76b118 Dimitris Aragiorgis
        try:
71 ad76b118 Dimitris Aragiorgis
            result = eval(code_obj, globals(), locals())
72 ad76b118 Dimitris Aragiorgis
        except TypeError as ex:
73 ad76b118 Dimitris Aragiorgis
            message = "failed to execute {0}: {1}".format(code, ex)
74 ad76b118 Dimitris Aragiorgis
            logger.warning(message)
75 ad76b118 Dimitris Aragiorgis
            raise EditorError(message)
76 ad76b118 Dimitris Aragiorgis
        if not result:
77 ad76b118 Dimitris Aragiorgis
            raise EditorError("cannot process line '{0}' with {1}".format(
78 ad76b118 Dimitris Aragiorgis
                              line, code))
79 ad76b118 Dimitris Aragiorgis
        elif isinstance(result, list) or isinstance(result, tuple):
80 ad76b118 Dimitris Aragiorgis
            line = ' '.join([str(res_element) for res_element in result])
81 ad76b118 Dimitris Aragiorgis
        else:
82 ad76b118 Dimitris Aragiorgis
            line = str(result)
83 ad76b118 Dimitris Aragiorgis
        return line
84 ad76b118 Dimitris Aragiorgis
85 ad76b118 Dimitris Aragiorgis
    def edit_line(self, line):
86 ad76b118 Dimitris Aragiorgis
        """Edits a single line using the code expression."""
87 ad76b118 Dimitris Aragiorgis
        for code, code_obj in self.code_objs.items():
88 ad76b118 Dimitris Aragiorgis
            line = self.__edit_line(line, code, code_obj)
89 ad76b118 Dimitris Aragiorgis
        return line
90 ad76b118 Dimitris Aragiorgis
91 ad76b118 Dimitris Aragiorgis
    def edit_file(self, file_name):
92 ad76b118 Dimitris Aragiorgis
        """Edit file in place, returns a list of modifications (unified diff).
93 ad76b118 Dimitris Aragiorgis

94 ad76b118 Dimitris Aragiorgis
        Arguments:
95 ad76b118 Dimitris Aragiorgis
        file_name -- The name of the file.
96 ad76b118 Dimitris Aragiorgis
        dry_run -- only return differences, but do not edit the file.
97 ad76b118 Dimitris Aragiorgis
        """
98 ad76b118 Dimitris Aragiorgis
        with open(file_name, "r") as from_file:
99 ad76b118 Dimitris Aragiorgis
            from_lines = from_file.readlines()
100 ad76b118 Dimitris Aragiorgis
            to_lines = [self.edit_line(line) for line in from_lines]
101 ad76b118 Dimitris Aragiorgis
            diffs = difflib.unified_diff(from_lines, to_lines,
102 ad76b118 Dimitris Aragiorgis
                                         fromfile=file_name, tofile='<new>')
103 ad76b118 Dimitris Aragiorgis
        if not self.dry_run:
104 ad76b118 Dimitris Aragiorgis
            bak_file_name = file_name + ".bak"
105 ad76b118 Dimitris Aragiorgis
            if os.path.exists(bak_file_name):
106 ad76b118 Dimitris Aragiorgis
                raise EditorError("{0} already exists".format(bak_file_name))
107 ad76b118 Dimitris Aragiorgis
            try:
108 ad76b118 Dimitris Aragiorgis
                os.rename(file_name, bak_file_name)
109 ad76b118 Dimitris Aragiorgis
                with open(file_name, "w") as new_file:
110 ad76b118 Dimitris Aragiorgis
                    new_file.writelines(to_lines)
111 ad76b118 Dimitris Aragiorgis
                os.unlink(bak_file_name)
112 ad76b118 Dimitris Aragiorgis
            except:
113 ad76b118 Dimitris Aragiorgis
                os.rename(bak_file_name, file_name)
114 ad76b118 Dimitris Aragiorgis
                raise
115 ad76b118 Dimitris Aragiorgis
        return list(diffs)
116 ad76b118 Dimitris Aragiorgis
117 ad76b118 Dimitris Aragiorgis
    def append_code_expr(self, code):
118 ad76b118 Dimitris Aragiorgis
        """Compiles argument and adds it to the list of code objects."""
119 ad76b118 Dimitris Aragiorgis
        assert(isinstance(code, str))  # expect a string.
120 ad76b118 Dimitris Aragiorgis
        logger.debug("compiling code {0}...".format(code))
121 ad76b118 Dimitris Aragiorgis
        try:
122 ad76b118 Dimitris Aragiorgis
            code_obj = compile(code, '<string>', 'eval')
123 ad76b118 Dimitris Aragiorgis
            self.code_objs[code] = code_obj
124 ad76b118 Dimitris Aragiorgis
        except SyntaxError as syntax_err:
125 ad76b118 Dimitris Aragiorgis
            logger.error("cannot compile {0}: {1}".format(
126 ad76b118 Dimitris Aragiorgis
                code, syntax_err))
127 ad76b118 Dimitris Aragiorgis
            raise
128 ad76b118 Dimitris Aragiorgis
        logger.debug("compiled code {0}".format(code))
129 ad76b118 Dimitris Aragiorgis
130 ad76b118 Dimitris Aragiorgis
    def set_code_expr(self, codes):
131 ad76b118 Dimitris Aragiorgis
        """Convenience: sets all the code expressions at once."""
132 ad76b118 Dimitris Aragiorgis
        self.code_objs = dict()
133 ad76b118 Dimitris Aragiorgis
        self._codes = []
134 ad76b118 Dimitris Aragiorgis
        for code in codes:
135 ad76b118 Dimitris Aragiorgis
            self.append_code_expr(code)
136 ad76b118 Dimitris Aragiorgis
137 ad76b118 Dimitris Aragiorgis
    def import_module(self, module):  # pylint: disable=R0201
138 ad76b118 Dimitris Aragiorgis
        """Imports module that are needed for the code expr to compile.
139 ad76b118 Dimitris Aragiorgis

140 ad76b118 Dimitris Aragiorgis
        Argument:
141 ad76b118 Dimitris Aragiorgis
        module -- can be scalar string or a list of strings.
142 ad76b118 Dimitris Aragiorgis
        """
143 ad76b118 Dimitris Aragiorgis
        if isinstance(module, list):
144 ad76b118 Dimitris Aragiorgis
            all_modules = module
145 ad76b118 Dimitris Aragiorgis
        else:
146 ad76b118 Dimitris Aragiorgis
            all_modules = [module]
147 ad76b118 Dimitris Aragiorgis
        for mod in all_modules:
148 ad76b118 Dimitris Aragiorgis
            globals()[mod] = __import__(mod.strip())
149 ad76b118 Dimitris Aragiorgis
150 ad76b118 Dimitris Aragiorgis
151 ad76b118 Dimitris Aragiorgis
def parse_command_line(argv):
152 ad76b118 Dimitris Aragiorgis
    """Parses command line argument. See -h option
153 ad76b118 Dimitris Aragiorgis

154 ad76b118 Dimitris Aragiorgis
    argv -- arguments on the command line including the caller file.
155 ad76b118 Dimitris Aragiorgis
    """
156 ad76b118 Dimitris Aragiorgis
    example = """
157 ad76b118 Dimitris Aragiorgis
    example: {0} -e "re.sub('failIf', 'assertFalse', line)" *.py
158 ad76b118 Dimitris Aragiorgis
    """
159 ad76b118 Dimitris Aragiorgis
    example = example.format(os.path.basename(argv[0]))
160 ad76b118 Dimitris Aragiorgis
    if sys.version_info[0] < 3:
161 ad76b118 Dimitris Aragiorgis
        parser = argparse.ArgumentParser(description="Python mass editor",
162 ad76b118 Dimitris Aragiorgis
                                         version=__version__,
163 ad76b118 Dimitris Aragiorgis
                                         epilog=example)
164 ad76b118 Dimitris Aragiorgis
    else:
165 ad76b118 Dimitris Aragiorgis
        parser = argparse.ArgumentParser(description="Python mass editor",
166 ad76b118 Dimitris Aragiorgis
                                         epilog=example)
167 ad76b118 Dimitris Aragiorgis
        parser.add_argument("-v", "--version", action="version",
168 ad76b118 Dimitris Aragiorgis
                            version="%(prog)s {}".format(__version__))
169 ad76b118 Dimitris Aragiorgis
    parser.add_argument("-w", "--write", dest="dry_run",
170 ad76b118 Dimitris Aragiorgis
                        action="store_false", default=True,
171 ad76b118 Dimitris Aragiorgis
                        help="modify target file(s) in place. "
172 ad76b118 Dimitris Aragiorgis
                        "Shows diff otherwise.")
173 ad76b118 Dimitris Aragiorgis
    parser.add_argument("-V", "--verbose", dest="verbose_count",
174 ad76b118 Dimitris Aragiorgis
                        action="count", default=0,
175 ad76b118 Dimitris Aragiorgis
                        help="increases log verbosity (can be specified "
176 ad76b118 Dimitris Aragiorgis
                        "multiple times)")
177 ad76b118 Dimitris Aragiorgis
    parser.add_argument('-e', "--expression", dest="expressions", nargs=1,
178 ad76b118 Dimitris Aragiorgis
                        help="Python expressions to be applied on all files. "
179 ad76b118 Dimitris Aragiorgis
                        "Use the line variable to reference the current line.")
180 ad76b118 Dimitris Aragiorgis
    parser.add_argument("-s", "--start", dest="start_dir",
181 ad76b118 Dimitris Aragiorgis
                        help="Starting directory in which to look for the "
182 ad76b118 Dimitris Aragiorgis
                        "files. If there is one pattern only and it includes "
183 ad76b118 Dimitris Aragiorgis
                        "a directory, the start dir will be that directory "
184 ad76b118 Dimitris Aragiorgis
                        "and the max depth level will be set to 1.")
185 ad76b118 Dimitris Aragiorgis
    parser.add_argument('-m', "--max-depth-level", type=int, dest="max_depth",
186 ad76b118 Dimitris Aragiorgis
                        help="Maximum depth when walking subdirectories.")
187 ad76b118 Dimitris Aragiorgis
    parser.add_argument('-o', '--output', metavar="output",
188 ad76b118 Dimitris Aragiorgis
                        type=argparse.FileType('w'), default=sys.stdout,
189 ad76b118 Dimitris Aragiorgis
                        help="redirect output to a file")
190 ad76b118 Dimitris Aragiorgis
    parser.add_argument('patterns', metavar="pattern", nargs='+',
191 ad76b118 Dimitris Aragiorgis
                        help="file patterns to process.")
192 ad76b118 Dimitris Aragiorgis
    arguments = parser.parse_args(argv[1:])
193 ad76b118 Dimitris Aragiorgis
    # Sets log level to WARN going more verbose for each new -V.
194 ad76b118 Dimitris Aragiorgis
    logger.setLevel(max(3 - arguments.verbose_count, 0) * 10)
195 ad76b118 Dimitris Aragiorgis
    return arguments
196 ad76b118 Dimitris Aragiorgis
197 ad76b118 Dimitris Aragiorgis
198 ad76b118 Dimitris Aragiorgis
def edit_files(patterns, expressions,  # pylint: disable=R0913, R0914
199 ad76b118 Dimitris Aragiorgis
               start_dir=None, max_depth=1, dry_run=True,
200 ad76b118 Dimitris Aragiorgis
               output=sys.stdout):
201 ad76b118 Dimitris Aragiorgis
    """Edits the files that match patterns with python expressions. Each
202 ad76b118 Dimitris Aragiorgis
    expression is run (using eval()) line by line on each input file.
203 ad76b118 Dimitris Aragiorgis

204 ad76b118 Dimitris Aragiorgis
    Keyword Arguments:
205 ad76b118 Dimitris Aragiorgis
    max_depth -- maximum recursion level when looking for file matches.
206 ad76b118 Dimitris Aragiorgis
    start_dir -- directory where to start the file search.
207 ad76b118 Dimitris Aragiorgis
    dry_run -- only display differences if True. Save modified file otherwise.
208 ad76b118 Dimitris Aragiorgis
    output -- handle where the output should be redirected.
209 ad76b118 Dimitris Aragiorgis
    """
210 ad76b118 Dimitris Aragiorgis
    # Makes for a better diagnostic because str are also iterable.
211 ad76b118 Dimitris Aragiorgis
    assert not isinstance(patterns, str), "patterns should be a list"
212 ad76b118 Dimitris Aragiorgis
    assert not isinstance(expressions, str), "expressions should be a list"
213 ad76b118 Dimitris Aragiorgis
214 ad76b118 Dimitris Aragiorgis
    # Shortcut: if there is only one pattern, make sure we process just that.
215 ad76b118 Dimitris Aragiorgis
    if len(patterns) == 1 and not start_dir:
216 ad76b118 Dimitris Aragiorgis
        pattern = patterns[0]
217 ad76b118 Dimitris Aragiorgis
        directory = os.path.dirname(pattern)
218 ad76b118 Dimitris Aragiorgis
        if directory:
219 ad76b118 Dimitris Aragiorgis
            patterns = [os.path.basename(pattern)]
220 ad76b118 Dimitris Aragiorgis
            start_dir = directory
221 ad76b118 Dimitris Aragiorgis
            max_depth = 1
222 ad76b118 Dimitris Aragiorgis
223 ad76b118 Dimitris Aragiorgis
    processed_paths = []
224 ad76b118 Dimitris Aragiorgis
    editor = Editor(dry_run=dry_run)
225 ad76b118 Dimitris Aragiorgis
    if expressions:
226 ad76b118 Dimitris Aragiorgis
        editor.set_code_expr(expressions)
227 ad76b118 Dimitris Aragiorgis
    if not start_dir:
228 ad76b118 Dimitris Aragiorgis
        start_dir = os.getcwd()
229 ad76b118 Dimitris Aragiorgis
    for root, dirs, files in os.walk(start_dir):  # pylint: disable=W0612
230 ad76b118 Dimitris Aragiorgis
        if max_depth is not None:
231 ad76b118 Dimitris Aragiorgis
            relpath = os.path.relpath(root, start=start_dir)
232 ad76b118 Dimitris Aragiorgis
            depth = len(relpath.split(os.sep))
233 ad76b118 Dimitris Aragiorgis
            if depth > max_depth:
234 ad76b118 Dimitris Aragiorgis
                continue
235 ad76b118 Dimitris Aragiorgis
        names = []
236 ad76b118 Dimitris Aragiorgis
        for pattern in patterns:
237 ad76b118 Dimitris Aragiorgis
            names += fnmatch.filter(files, pattern)
238 ad76b118 Dimitris Aragiorgis
        for name in names:
239 ad76b118 Dimitris Aragiorgis
            path = os.path.join(root, name)
240 ad76b118 Dimitris Aragiorgis
            processed_paths.append(os.path.abspath(path))
241 ad76b118 Dimitris Aragiorgis
            diffs = editor.edit_file(path)
242 ad76b118 Dimitris Aragiorgis
            if dry_run:
243 ad76b118 Dimitris Aragiorgis
                output.write("".join(diffs))
244 ad76b118 Dimitris Aragiorgis
    if output != sys.stdout:
245 ad76b118 Dimitris Aragiorgis
        output.close()
246 ad76b118 Dimitris Aragiorgis
    return processed_paths
247 ad76b118 Dimitris Aragiorgis
248 ad76b118 Dimitris Aragiorgis
249 ad76b118 Dimitris Aragiorgis
def command_line(argv):
250 ad76b118 Dimitris Aragiorgis
    """Instantiate an editor and process arguments.
251 ad76b118 Dimitris Aragiorgis

252 ad76b118 Dimitris Aragiorgis
    Optional argument:
253 ad76b118 Dimitris Aragiorgis
    processed_paths -- paths processed are appended to the list.
254 ad76b118 Dimitris Aragiorgis
    """
255 ad76b118 Dimitris Aragiorgis
    arguments = parse_command_line(argv)
256 ad76b118 Dimitris Aragiorgis
    return edit_files(arguments.patterns, arguments.expressions,
257 ad76b118 Dimitris Aragiorgis
                      start_dir=arguments.start_dir,
258 ad76b118 Dimitris Aragiorgis
                      max_depth=arguments.max_depth,
259 ad76b118 Dimitris Aragiorgis
                      dry_run=arguments.dry_run,
260 ad76b118 Dimitris Aragiorgis
                      output=arguments.output)
261 ad76b118 Dimitris Aragiorgis
262 ad76b118 Dimitris Aragiorgis
263 ad76b118 Dimitris Aragiorgis
if __name__ == "__main__":
264 ad76b118 Dimitris Aragiorgis
    logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
265 ad76b118 Dimitris Aragiorgis
    try:
266 ad76b118 Dimitris Aragiorgis
        command_line(sys.argv)
267 ad76b118 Dimitris Aragiorgis
    finally:
268 ad76b118 Dimitris Aragiorgis
        logging.shutdown()