• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2# -*- coding: UTF-8 -*-
3"""
4Provides a command container for additional tox commands, used in "tox.ini".
5
6COMMANDS:
7
8  * copytree
9  * copy
10  * py2to3
11
12REQUIRES:
13  * argparse
14"""
15
16from glob import glob
17import argparse
18import inspect
19import os.path
20import shutil
21import sys
22
23__author__ = "Jens Engel"
24__copyright__ = "(c) 2013 by Jens Engel"
25__license__ = "BSD"
26
27# -----------------------------------------------------------------------------
28# CONSTANTS:
29# -----------------------------------------------------------------------------
30VERSION = "0.1.0"
31FORMATTER_CLASS = argparse.RawDescriptionHelpFormatter
32
33
34# -----------------------------------------------------------------------------
35# SUBCOMMAND: copytree
36# -----------------------------------------------------------------------------
37def command_copytree(args):
38    """
39    Copy one or more source directory(s) below a destination directory.
40    Parts of the destination directory path are created if needed.
41    Similar to the UNIX command: 'cp -R srcdir destdir'
42    """
43    for srcdir in args.srcdirs:
44        basename = os.path.basename(srcdir)
45        destdir2 = os.path.normpath(os.path.join(args.destdir, basename))
46        if os.path.exists(destdir2):
47            shutil.rmtree(destdir2)
48        sys.stdout.write("copytree: %s => %s\n" % (srcdir, destdir2))
49        shutil.copytree(srcdir, destdir2)
50    return 0
51
52
53def setup_parser_copytree(parser):
54    parser.add_argument("srcdirs", nargs="+", help="Source directory(s)")
55    parser.add_argument("destdir", help="Destination directory")
56
57
58command_copytree.usage = "%(prog)s srcdir... destdir"
59command_copytree.short = "Copy source dir(s) below a destination directory."
60command_copytree.setup_parser = setup_parser_copytree
61
62
63# -----------------------------------------------------------------------------
64# SUBCOMMAND: copy
65# -----------------------------------------------------------------------------
66def command_copy(args):
67    """
68    Copy one or more source-files(s) to a destpath (destfile or destdir).
69    Destdir mode is used if:
70      * More than one srcfile is provided
71      * Last parameter ends with a slash ("/").
72      * Last parameter is an existing directory
73
74    Destination directory path is created if needed.
75    Similar to the UNIX command: 'cp srcfile... destpath'
76    """
77    sources = args.sources
78    destpath = args.destpath
79    source_files = []
80    for file_ in sources:
81        if "*" in file_:
82            selected = glob(file_)
83            source_files.extend(selected)
84        elif os.path.isfile(file_):
85            source_files.append(file_)
86
87    if destpath.endswith("/") or os.path.isdir(destpath) or len(sources) > 1:
88        # -- DESTDIR-MODE: Last argument is a directory.
89        destdir = destpath
90    else:
91        # -- DESTFILE-MODE: Copy (and rename) one file.
92        assert len(source_files) == 1
93        destdir = os.path.dirname(destpath)
94
95    # -- WORK-HORSE: Copy one or more files to destpath.
96    if not os.path.isdir(destdir):
97        sys.stdout.write("copy: Create dir %s\n" % destdir)
98        os.makedirs(destdir)
99    for source in source_files:
100        destname = os.path.join(destdir, os.path.basename(source))
101        sys.stdout.write("copy: %s => %s\n" % (source, destname))
102        shutil.copy(source, destname)
103    return 0
104
105
106def setup_parser_copy(parser):
107    parser.add_argument("sources", nargs="+", help="Source files.")
108    parser.add_argument("destpath", help="Destination path")
109
110
111command_copy.usage = "%(prog)s sources... destpath"
112command_copy.short = "Copy one or more source files to a destinition."
113command_copy.setup_parser = setup_parser_copy
114
115
116# -----------------------------------------------------------------------------
117# SUBCOMMAND: mkdir
118# -----------------------------------------------------------------------------
119def command_mkdir(args):
120    """
121    Create a non-existing directory (or more ...).
122    If the directory exists, the step is skipped.
123    Similar to the UNIX command: 'mkdir -p dir'
124    """
125    errors = 0
126    for directory in args.dirs:
127        if os.path.exists(directory):
128            if not os.path.isdir(directory):
129                # -- SANITY CHECK: directory exists, but as file...
130                sys.stdout.write("mkdir: %s\n" % directory)
131                sys.stdout.write("ERROR: Exists already, but as file...\n")
132                errors += 1
133        else:
134            # -- NORMAL CASE: Directory does not exits yet.
135            assert not os.path.isdir(directory)
136            sys.stdout.write("mkdir: %s\n" % directory)
137            os.makedirs(directory)
138    return errors
139
140
141def setup_parser_mkdir(parser):
142    parser.add_argument("dirs", nargs="+", help="Directory(s)")
143
144command_mkdir.usage = "%(prog)s dir..."
145command_mkdir.short = "Create non-existing directory (or more...)."
146command_mkdir.setup_parser = setup_parser_mkdir
147
148# -----------------------------------------------------------------------------
149# SUBCOMMAND: py2to3
150# -----------------------------------------------------------------------------
151def command_py2to3(args):
152    """
153    Apply '2to3' tool (Python2 to Python3 conversion tool) to Python sources.
154    """
155    from lib2to3.main import main
156    sys.exit(main("lib2to3.fixes", args=args.sources))
157
158
159def setup_parser4py2to3(parser):
160    parser.add_argument("sources", nargs="+", help="Source files.")
161
162
163command_py2to3.name = "2to3"
164command_py2to3.usage = "%(prog)s sources..."
165command_py2to3.short = "Apply python's 2to3 tool to Python sources."
166command_py2to3.setup_parser = setup_parser4py2to3
167
168
169# -----------------------------------------------------------------------------
170# COMMAND HELPERS/UTILS:
171# -----------------------------------------------------------------------------
172def discover_commands():
173    commands = []
174    for name, func in inspect.getmembers(inspect.getmodule(toxcmd_main)):
175        if name.startswith("__"):
176            continue
177        if name.startswith("command_") and callable(func):
178            command_name0 = name.replace("command_", "")
179            command_name = getattr(func, "name", command_name0)
180            commands.append(Command(command_name, func))
181    return commands
182
183
184class Command(object):
185    def __init__(self, name, func):
186        assert isinstance(name, basestring)
187        assert callable(func)
188        self.name = name
189        self.func = func
190        self.parser = None
191
192    def setup_parser(self, command_parser):
193        setup_parser = getattr(self.func, "setup_parser", None)
194        if setup_parser and callable(setup_parser):
195            setup_parser(command_parser)
196        else:
197            command_parser.add_argument("args", nargs="*")
198
199    @property
200    def usage(self):
201        usage = getattr(self.func, "usage", None)
202        return usage
203
204    @property
205    def short_description(self):
206        short_description = getattr(self.func, "short", "")
207        return short_description
208
209    @property
210    def description(self):
211        return inspect.getdoc(self.func)
212
213    def __call__(self, args):
214        return self.func(args)
215
216
217# -----------------------------------------------------------------------------
218# MAIN-COMMAND:
219# -----------------------------------------------------------------------------
220def toxcmd_main(args=None):
221    """Command util with subcommands for tox environments."""
222    usage = "USAGE: %(prog)s [OPTIONS] COMMAND args..."
223    if args is None:
224        args = sys.argv[1:]
225
226    # -- STEP: Build command-line parser.
227    parser = argparse.ArgumentParser(description=inspect.getdoc(toxcmd_main),
228                                     formatter_class=FORMATTER_CLASS)
229    common_parser = parser.add_argument_group("Common options")
230    common_parser.add_argument("--version", action="version", version=VERSION)
231    subparsers = parser.add_subparsers(help="commands")
232    for command in discover_commands():
233        command_parser = subparsers.add_parser(command.name,
234                                               usage=command.usage,
235                                               description=command.description,
236                                               help=command.short_description,
237                                               formatter_class=FORMATTER_CLASS)
238        command_parser.set_defaults(func=command)
239        command.setup_parser(command_parser)
240        command.parser = command_parser
241
242    # -- STEP: Process command-line and run command.
243    options = parser.parse_args(args)
244    command_function = options.func
245    return command_function(options)
246
247
248# -----------------------------------------------------------------------------
249# MAIN:
250# -----------------------------------------------------------------------------
251if __name__ == "__main__":
252    sys.exit(toxcmd_main())
253