aboutsummaryrefslogtreecommitdiff
path: root/bin/toxcmd.py
blob: 38bb6d5529d804a0d8a88b37ecceb5410904942e (plain)
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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
"""
Provides a command container for additional tox commands, used in "tox.ini".

COMMANDS:

  * copytree
  * copy
  * py2to3

REQUIRES:
  * argparse
"""

from glob import glob
import argparse
import inspect
import os.path
import shutil
import sys

__author__ = "Jens Engel"
__copyright__ = "(c) 2013 by Jens Engel"
__license__ = "BSD"

# -----------------------------------------------------------------------------
# CONSTANTS:
# -----------------------------------------------------------------------------
VERSION = "0.1.0"
FORMATTER_CLASS = argparse.RawDescriptionHelpFormatter


# -----------------------------------------------------------------------------
# SUBCOMMAND: copytree
# -----------------------------------------------------------------------------
def command_copytree(args):
    """
    Copy one or more source directory(s) below a destination directory.
    Parts of the destination directory path are created if needed.
    Similar to the UNIX command: 'cp -R srcdir destdir'
    """
    for srcdir in args.srcdirs:
        basename = os.path.basename(srcdir)
        destdir2 = os.path.normpath(os.path.join(args.destdir, basename))
        if os.path.exists(destdir2):
            shutil.rmtree(destdir2)
        sys.stdout.write("copytree: %s => %s\n" % (srcdir, destdir2))
        shutil.copytree(srcdir, destdir2)
    return 0


def setup_parser_copytree(parser):
    parser.add_argument("srcdirs", nargs="+", help="Source directory(s)")
    parser.add_argument("destdir", help="Destination directory")


command_copytree.usage = "%(prog)s srcdir... destdir"
command_copytree.short = "Copy source dir(s) below a destination directory."
command_copytree.setup_parser = setup_parser_copytree


# -----------------------------------------------------------------------------
# SUBCOMMAND: copy
# -----------------------------------------------------------------------------
def command_copy(args):
    """
    Copy one or more source-files(s) to a destpath (destfile or destdir).
    Destdir mode is used if:
      * More than one srcfile is provided
      * Last parameter ends with a slash ("/").
      * Last parameter is an existing directory

    Destination directory path is created if needed.
    Similar to the UNIX command: 'cp srcfile... destpath'
    """
    sources = args.sources
    destpath = args.destpath
    source_files = []
    for file_ in sources:
        if "*" in file_:
            selected = glob(file_)
            source_files.extend(selected)
        elif os.path.isfile(file_):
            source_files.append(file_)

    if destpath.endswith("/") or os.path.isdir(destpath) or len(sources) > 1:
        # -- DESTDIR-MODE: Last argument is a directory.
        destdir = destpath
    else:
        # -- DESTFILE-MODE: Copy (and rename) one file.
        assert len(source_files) == 1
        destdir = os.path.dirname(destpath)

    # -- WORK-HORSE: Copy one or more files to destpath.
    if not os.path.isdir(destdir):
        sys.stdout.write("copy: Create dir %s\n" % destdir)
        os.makedirs(destdir)
    for source in source_files:
        destname = os.path.join(destdir, os.path.basename(source))
        sys.stdout.write("copy: %s => %s\n" % (source, destname))
        shutil.copy(source, destname)
    return 0


def setup_parser_copy(parser):
    parser.add_argument("sources", nargs="+", help="Source files.")
    parser.add_argument("destpath", help="Destination path")


command_copy.usage = "%(prog)s sources... destpath"
command_copy.short = "Copy one or more source files to a destinition."
command_copy.setup_parser = setup_parser_copy


# -----------------------------------------------------------------------------
# SUBCOMMAND: mkdir
# -----------------------------------------------------------------------------
def command_mkdir(args):
    """
    Create a non-existing directory (or more ...).
    If the directory exists, the step is skipped.
    Similar to the UNIX command: 'mkdir -p dir'
    """
    errors = 0
    for directory in args.dirs:
        if os.path.exists(directory):
            if not os.path.isdir(directory):
                # -- SANITY CHECK: directory exists, but as file...
                sys.stdout.write("mkdir: %s\n" % directory)
                sys.stdout.write("ERROR: Exists already, but as file...\n")
                errors += 1
        else:
            # -- NORMAL CASE: Directory does not exits yet.
            assert not os.path.isdir(directory)
            sys.stdout.write("mkdir: %s\n" % directory)
            os.makedirs(directory)
    return errors


def setup_parser_mkdir(parser):
    parser.add_argument("dirs", nargs="+", help="Directory(s)")

command_mkdir.usage = "%(prog)s dir..."
command_mkdir.short = "Create non-existing directory (or more...)."
command_mkdir.setup_parser = setup_parser_mkdir

# -----------------------------------------------------------------------------
# SUBCOMMAND: py2to3
# -----------------------------------------------------------------------------
def command_py2to3(args):
    """
    Apply '2to3' tool (Python2 to Python3 conversion tool) to Python sources.
    """
    from lib2to3.main import main
    sys.exit(main("lib2to3.fixes", args=args.sources))


def setup_parser4py2to3(parser):
    parser.add_argument("sources", nargs="+", help="Source files.")


command_py2to3.name = "2to3"
command_py2to3.usage = "%(prog)s sources..."
command_py2to3.short = "Apply python's 2to3 tool to Python sources."
command_py2to3.setup_parser = setup_parser4py2to3


# -----------------------------------------------------------------------------
# COMMAND HELPERS/UTILS:
# -----------------------------------------------------------------------------
def discover_commands():
    commands = []
    for name, func in inspect.getmembers(inspect.getmodule(toxcmd_main)):
        if name.startswith("__"):
            continue
        if name.startswith("command_") and callable(func):
            command_name0 = name.replace("command_", "")
            command_name = getattr(func, "name", command_name0)
            commands.append(Command(command_name, func))
    return commands


class Command(object):
    def __init__(self, name, func):
        assert isinstance(name, basestring)
        assert callable(func)
        self.name = name
        self.func = func
        self.parser = None

    def setup_parser(self, command_parser):
        setup_parser = getattr(self.func, "setup_parser", None)
        if setup_parser and callable(setup_parser):
            setup_parser(command_parser)
        else:
            command_parser.add_argument("args", nargs="*")

    @property
    def usage(self):
        usage = getattr(self.func, "usage", None)
        return usage

    @property
    def short_description(self):
        short_description = getattr(self.func, "short", "")
        return short_description

    @property
    def description(self):
        return inspect.getdoc(self.func)

    def __call__(self, args):
        return self.func(args)


# -----------------------------------------------------------------------------
# MAIN-COMMAND:
# -----------------------------------------------------------------------------
def toxcmd_main(args=None):
    """Command util with subcommands for tox environments."""
    usage = "USAGE: %(prog)s [OPTIONS] COMMAND args..."
    if args is None:
        args = sys.argv[1:]

    # -- STEP: Build command-line parser.
    parser = argparse.ArgumentParser(description=inspect.getdoc(toxcmd_main),
                                     formatter_class=FORMATTER_CLASS)
    common_parser = parser.add_argument_group("Common options")
    common_parser.add_argument("--version", action="version", version=VERSION)
    subparsers = parser.add_subparsers(help="commands")
    for command in discover_commands():
        command_parser = subparsers.add_parser(command.name,
                                               usage=command.usage,
                                               description=command.description,
                                               help=command.short_description,
                                               formatter_class=FORMATTER_CLASS)
        command_parser.set_defaults(func=command)
        command.setup_parser(command_parser)
        command.parser = command_parser

    # -- STEP: Process command-line and run command.
    options = parser.parse_args(args)
    command_function = options.func
    return command_function(options)


# -----------------------------------------------------------------------------
# MAIN:
# -----------------------------------------------------------------------------
if __name__ == "__main__":
    sys.exit(toxcmd_main())