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