| Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1 | """Main command line parser.  Implements the pysetup script.""" | 
|  | 2 |  | 
|  | 3 | import os | 
|  | 4 | import re | 
|  | 5 | import sys | 
|  | 6 | import getopt | 
|  | 7 | import logging | 
| Tarek Ziade | b1b6e13 | 2011-05-30 12:07:49 +0200 | [diff] [blame] | 8 | from copy import copy | 
| Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 9 |  | 
|  | 10 | from packaging import logger | 
|  | 11 | from packaging.dist import Distribution | 
|  | 12 | from packaging.util import _is_archive_file | 
|  | 13 | from packaging.command import get_command_class, STANDARD_COMMANDS | 
|  | 14 | from packaging.install import install, install_local_project, remove | 
|  | 15 | from packaging.database import get_distribution, get_distributions | 
|  | 16 | from packaging.depgraph import generate_graph | 
|  | 17 | from packaging.fancy_getopt import FancyGetopt | 
|  | 18 | from packaging.errors import (PackagingArgError, PackagingError, | 
|  | 19 | PackagingModuleError, PackagingClassError, | 
|  | 20 | CCompilerError) | 
|  | 21 |  | 
|  | 22 |  | 
|  | 23 | command_re = re.compile(r'^[a-zA-Z]([a-zA-Z0-9_]*)$') | 
|  | 24 |  | 
|  | 25 | common_usage = """\ | 
|  | 26 | Actions: | 
|  | 27 | %(actions)s | 
|  | 28 |  | 
|  | 29 | To get more help on an action, use: | 
|  | 30 |  | 
|  | 31 | pysetup action --help | 
|  | 32 | """ | 
|  | 33 |  | 
|  | 34 | create_usage = """\ | 
|  | 35 | Usage: pysetup create | 
|  | 36 | or: pysetup create --help | 
|  | 37 |  | 
|  | 38 | Create a new Python package. | 
|  | 39 | """ | 
|  | 40 |  | 
|  | 41 | graph_usage = """\ | 
|  | 42 | Usage: pysetup graph dist | 
|  | 43 | or: pysetup graph --help | 
|  | 44 |  | 
|  | 45 | Print dependency graph for the distribution. | 
|  | 46 |  | 
|  | 47 | positional arguments: | 
|  | 48 | dist  installed distribution name | 
|  | 49 | """ | 
|  | 50 |  | 
|  | 51 | install_usage = """\ | 
|  | 52 | Usage: pysetup install [dist] | 
|  | 53 | or: pysetup install [archive] | 
|  | 54 | or: pysetup install [src_dir] | 
|  | 55 | or: pysetup install --help | 
|  | 56 |  | 
|  | 57 | Install a Python distribution from the indexes, source directory, or sdist. | 
|  | 58 |  | 
|  | 59 | positional arguments: | 
|  | 60 | archive  path to source distribution (zip, tar.gz) | 
|  | 61 | dist     distribution name to install from the indexes | 
|  | 62 | scr_dir  path to source directory | 
|  | 63 |  | 
|  | 64 | """ | 
|  | 65 |  | 
|  | 66 | metadata_usage = """\ | 
|  | 67 | Usage: pysetup metadata [dist] [-f field ...] | 
|  | 68 | or: pysetup metadata [dist] [--all] | 
|  | 69 | or: pysetup metadata --help | 
|  | 70 |  | 
|  | 71 | Print metadata for the distribution. | 
|  | 72 |  | 
|  | 73 | positional arguments: | 
|  | 74 | dist  installed distribution name | 
|  | 75 |  | 
|  | 76 | optional arguments: | 
|  | 77 | -f     metadata field to print | 
|  | 78 | --all  print all metadata fields | 
|  | 79 | """ | 
|  | 80 |  | 
|  | 81 | remove_usage = """\ | 
|  | 82 | Usage: pysetup remove dist [-y] | 
|  | 83 | or: pysetup remove --help | 
|  | 84 |  | 
|  | 85 | Uninstall a Python distribution. | 
|  | 86 |  | 
|  | 87 | positional arguments: | 
|  | 88 | dist  installed distribution name | 
|  | 89 |  | 
|  | 90 | optional arguments: | 
|  | 91 | -y  auto confirm package removal | 
|  | 92 | """ | 
|  | 93 |  | 
|  | 94 | run_usage = """\ | 
|  | 95 | Usage: pysetup run [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...] | 
|  | 96 | or: pysetup run --help | 
|  | 97 | or: pysetup run --list-commands | 
|  | 98 | or: pysetup run cmd --help | 
|  | 99 | """ | 
|  | 100 |  | 
|  | 101 | list_usage = """\ | 
|  | 102 | Usage: pysetup list dist [dist ...] | 
|  | 103 | or: pysetup list --help | 
|  | 104 | or: pysetup list --all | 
|  | 105 |  | 
|  | 106 | Print name, version and location for the matching installed distributions. | 
|  | 107 |  | 
|  | 108 | positional arguments: | 
|  | 109 | dist  installed distribution name | 
|  | 110 |  | 
|  | 111 | optional arguments: | 
|  | 112 | --all  list all installed distributions | 
|  | 113 | """ | 
|  | 114 |  | 
|  | 115 | search_usage = """\ | 
|  | 116 | Usage: pysetup search [project] [--simple [url]] [--xmlrpc [url] [--fieldname value ...] --operator or|and] | 
|  | 117 | or: pysetup search --help | 
|  | 118 |  | 
|  | 119 | Search the indexes for the matching projects. | 
|  | 120 |  | 
|  | 121 | positional arguments: | 
|  | 122 | project     the project pattern to search for | 
|  | 123 |  | 
|  | 124 | optional arguments: | 
|  | 125 | --xmlrpc [url]      wether to use the xmlrpc index or not. If an url is | 
|  | 126 | specified, it will be used rather than the default one. | 
|  | 127 |  | 
|  | 128 | --simple [url]      wether to use the simple index or not. If an url is | 
|  | 129 | specified, it will be used rather than the default one. | 
|  | 130 |  | 
|  | 131 | --fieldname value   Make a search on this field. Can only be used if | 
|  | 132 | --xmlrpc has been selected or is the default index. | 
|  | 133 |  | 
|  | 134 | --operator or|and   Defines what is the operator to use when doing xmlrpc | 
|  | 135 | searchs with multiple fieldnames. Can only be used if | 
|  | 136 | --xmlrpc has been selected or is the default index. | 
|  | 137 | """ | 
|  | 138 |  | 
|  | 139 | global_options = [ | 
|  | 140 | # The fourth entry for verbose means that it can be repeated. | 
|  | 141 | ('verbose', 'v', "run verbosely (default)", True), | 
|  | 142 | ('quiet', 'q', "run quietly (turns verbosity off)"), | 
|  | 143 | ('dry-run', 'n', "don't actually do anything"), | 
|  | 144 | ('help', 'h', "show detailed help message"), | 
|  | 145 | ('no-user-cfg', None, 'ignore pydistutils.cfg in your home directory'), | 
|  | 146 | ('version', None, 'Display the version'), | 
|  | 147 | ] | 
|  | 148 |  | 
|  | 149 | negative_opt = {'quiet': 'verbose'} | 
|  | 150 |  | 
|  | 151 | display_options = [ | 
|  | 152 | ('help-commands', None, "list all available commands"), | 
|  | 153 | ] | 
|  | 154 |  | 
|  | 155 | display_option_names = [x[0].replace('-', '_') for x in display_options] | 
|  | 156 |  | 
|  | 157 |  | 
|  | 158 | def _parse_args(args, options, long_options): | 
|  | 159 | """Transform sys.argv input into a dict. | 
|  | 160 |  | 
|  | 161 | :param args: the args to parse (i.e sys.argv) | 
|  | 162 | :param options: the list of options to pass to getopt | 
|  | 163 | :param long_options: the list of string with the names of the long options | 
|  | 164 | to be passed to getopt. | 
|  | 165 |  | 
|  | 166 | The function returns a dict with options/long_options as keys and matching | 
|  | 167 | values as values. | 
|  | 168 | """ | 
|  | 169 | optlist, args = getopt.gnu_getopt(args, options, long_options) | 
|  | 170 | optdict = {} | 
|  | 171 | optdict['args'] = args | 
|  | 172 | for k, v in optlist: | 
|  | 173 | k = k.lstrip('-') | 
|  | 174 | if k not in optdict: | 
|  | 175 | optdict[k] = [] | 
|  | 176 | if v: | 
|  | 177 | optdict[k].append(v) | 
|  | 178 | else: | 
|  | 179 | optdict[k].append(v) | 
|  | 180 | return optdict | 
|  | 181 |  | 
|  | 182 |  | 
|  | 183 | class action_help: | 
|  | 184 | """Prints a help message when the standard help flags: -h and --help | 
|  | 185 | are used on the commandline. | 
|  | 186 | """ | 
|  | 187 |  | 
|  | 188 | def __init__(self, help_msg): | 
|  | 189 | self.help_msg = help_msg | 
|  | 190 |  | 
|  | 191 | def __call__(self, f): | 
|  | 192 | def wrapper(*args, **kwargs): | 
|  | 193 | f_args = args[1] | 
|  | 194 | if '--help' in f_args or '-h' in f_args: | 
|  | 195 | print(self.help_msg) | 
|  | 196 | return | 
|  | 197 | return f(*args, **kwargs) | 
|  | 198 | return wrapper | 
|  | 199 |  | 
|  | 200 |  | 
|  | 201 | @action_help(create_usage) | 
|  | 202 | def _create(distpatcher, args, **kw): | 
|  | 203 | from packaging.create import main | 
|  | 204 | return main() | 
|  | 205 |  | 
|  | 206 |  | 
|  | 207 | @action_help(graph_usage) | 
|  | 208 | def _graph(dispatcher, args, **kw): | 
|  | 209 | name = args[1] | 
|  | 210 | dist = get_distribution(name, use_egg_info=True) | 
|  | 211 | if dist is None: | 
|  | 212 | print('Distribution not found.') | 
|  | 213 | else: | 
|  | 214 | dists = get_distributions(use_egg_info=True) | 
|  | 215 | graph = generate_graph(dists) | 
|  | 216 | print(graph.repr_node(dist)) | 
|  | 217 |  | 
|  | 218 |  | 
|  | 219 | @action_help(install_usage) | 
|  | 220 | def _install(dispatcher, args, **kw): | 
|  | 221 | # first check if we are in a source directory | 
|  | 222 | if len(args) < 2: | 
|  | 223 | # are we inside a project dir? | 
|  | 224 | listing = os.listdir(os.getcwd()) | 
|  | 225 | if 'setup.py' in listing or 'setup.cfg' in listing: | 
|  | 226 | args.insert(1, os.getcwd()) | 
|  | 227 | else: | 
| Tarek Ziade | 5a5ce38 | 2011-05-31 12:09:34 +0200 | [diff] [blame] | 228 | logger.warning('No project to install.') | 
|  | 229 | return 1 | 
| Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 230 |  | 
| Tarek Ziade | b1b6e13 | 2011-05-30 12:07:49 +0200 | [diff] [blame] | 231 | target = args[1] | 
| Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 232 | # installing from a source dir or archive file? | 
| Tarek Ziade | b1b6e13 | 2011-05-30 12:07:49 +0200 | [diff] [blame] | 233 | if os.path.isdir(target) or _is_archive_file(target): | 
| Tarek Ziade | 5a5ce38 | 2011-05-31 12:09:34 +0200 | [diff] [blame] | 234 | if install_local_project(target): | 
|  | 235 | return 0 | 
|  | 236 | else: | 
|  | 237 | return 1 | 
| Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 238 | else: | 
|  | 239 | # download from PyPI | 
| Tarek Ziade | 5a5ce38 | 2011-05-31 12:09:34 +0200 | [diff] [blame] | 240 | if install(target): | 
|  | 241 | return 0 | 
|  | 242 | else: | 
|  | 243 | return 1 | 
| Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 244 |  | 
|  | 245 |  | 
|  | 246 | @action_help(metadata_usage) | 
|  | 247 | def _metadata(dispatcher, args, **kw): | 
|  | 248 | opts = _parse_args(args[1:], 'f:', ['all']) | 
|  | 249 | if opts['args']: | 
|  | 250 | name = opts['args'][0] | 
|  | 251 | dist = get_distribution(name, use_egg_info=True) | 
|  | 252 | if dist is None: | 
|  | 253 | logger.warning('%s not installed', name) | 
|  | 254 | return | 
|  | 255 | else: | 
|  | 256 | logger.info('searching local dir for metadata') | 
|  | 257 | dist = Distribution() | 
|  | 258 | dist.parse_config_files() | 
|  | 259 |  | 
|  | 260 | metadata = dist.metadata | 
|  | 261 |  | 
|  | 262 | if 'all' in opts: | 
|  | 263 | keys = metadata.keys() | 
|  | 264 | else: | 
|  | 265 | if 'f' in opts: | 
|  | 266 | keys = (k for k in opts['f'] if k in metadata) | 
|  | 267 | else: | 
|  | 268 | keys = () | 
|  | 269 |  | 
|  | 270 | for key in keys: | 
|  | 271 | if key in metadata: | 
|  | 272 | print(metadata._convert_name(key) + ':') | 
|  | 273 | value = metadata[key] | 
|  | 274 | if isinstance(value, list): | 
|  | 275 | for v in value: | 
|  | 276 | print('    ' + v) | 
|  | 277 | else: | 
|  | 278 | print('    ' + value.replace('\n', '\n    ')) | 
|  | 279 |  | 
|  | 280 |  | 
|  | 281 | @action_help(remove_usage) | 
|  | 282 | def _remove(distpatcher, args, **kw): | 
|  | 283 | opts = _parse_args(args[1:], 'y', []) | 
|  | 284 | if 'y' in opts: | 
|  | 285 | auto_confirm = True | 
|  | 286 | else: | 
|  | 287 | auto_confirm = False | 
|  | 288 |  | 
|  | 289 | for dist in set(opts['args']): | 
|  | 290 | try: | 
|  | 291 | remove(dist, auto_confirm=auto_confirm) | 
|  | 292 | except PackagingError: | 
|  | 293 | logger.warning('%s not installed', dist) | 
|  | 294 |  | 
|  | 295 |  | 
|  | 296 | @action_help(run_usage) | 
|  | 297 | def _run(dispatcher, args, **kw): | 
|  | 298 | parser = dispatcher.parser | 
|  | 299 | args = args[1:] | 
|  | 300 |  | 
|  | 301 | commands = STANDARD_COMMANDS  # + extra commands | 
|  | 302 |  | 
|  | 303 | if args == ['--list-commands']: | 
|  | 304 | print('List of available commands:') | 
|  | 305 | cmds = sorted(commands) | 
|  | 306 |  | 
|  | 307 | for cmd in cmds: | 
|  | 308 | cls = dispatcher.cmdclass.get(cmd) or get_command_class(cmd) | 
|  | 309 | desc = getattr(cls, 'description', | 
|  | 310 | '(no description available)') | 
|  | 311 | print('  %s: %s' % (cmd, desc)) | 
|  | 312 | return | 
|  | 313 |  | 
|  | 314 | while args: | 
|  | 315 | args = dispatcher._parse_command_opts(parser, args) | 
|  | 316 | if args is None: | 
|  | 317 | return | 
|  | 318 |  | 
|  | 319 | # create the Distribution class | 
|  | 320 | # need to feed setup.cfg here ! | 
|  | 321 | dist = Distribution() | 
|  | 322 |  | 
|  | 323 | # Find and parse the config file(s): they will override options from | 
|  | 324 | # the setup script, but be overridden by the command line. | 
|  | 325 |  | 
|  | 326 | # XXX still need to be extracted from Distribution | 
|  | 327 | dist.parse_config_files() | 
|  | 328 |  | 
|  | 329 | try: | 
|  | 330 | for cmd in dispatcher.commands: | 
|  | 331 | dist.run_command(cmd, dispatcher.command_options[cmd]) | 
|  | 332 |  | 
|  | 333 | except KeyboardInterrupt: | 
|  | 334 | raise SystemExit("interrupted") | 
|  | 335 | except (IOError, os.error, PackagingError, CCompilerError) as msg: | 
|  | 336 | raise SystemExit("error: " + str(msg)) | 
|  | 337 |  | 
|  | 338 | # XXX this is crappy | 
|  | 339 | return dist | 
|  | 340 |  | 
|  | 341 |  | 
|  | 342 | @action_help(list_usage) | 
|  | 343 | def _list(dispatcher, args, **kw): | 
|  | 344 | opts = _parse_args(args[1:], '', ['all']) | 
|  | 345 | dists = get_distributions(use_egg_info=True) | 
| Tarek Ziade | 441531f | 2011-05-31 09:18:24 +0200 | [diff] [blame] | 346 | if 'all' in opts or opts['args'] == []: | 
| Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 347 | results = dists | 
|  | 348 | else: | 
|  | 349 | results = [d for d in dists if d.name.lower() in opts['args']] | 
|  | 350 |  | 
| Tarek Ziade | 441531f | 2011-05-31 09:18:24 +0200 | [diff] [blame] | 351 | number = 0 | 
| Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 352 | for dist in results: | 
|  | 353 | print('%s %s at %s' % (dist.name, dist.metadata['version'], dist.path)) | 
| Tarek Ziade | e265597 | 2011-05-31 12:15:42 +0200 | [diff] [blame^] | 354 | number += 1 | 
| Tarek Ziade | 441531f | 2011-05-31 09:18:24 +0200 | [diff] [blame] | 355 |  | 
|  | 356 | print('') | 
|  | 357 | if number == 0: | 
|  | 358 | print('Nothing seems to be installed.') | 
|  | 359 | else: | 
|  | 360 | print('Found %d projects installed.' % number) | 
| Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 361 |  | 
|  | 362 |  | 
|  | 363 | @action_help(search_usage) | 
|  | 364 | def _search(dispatcher, args, **kw): | 
|  | 365 | """The search action. | 
|  | 366 |  | 
|  | 367 | It is able to search for a specific index (specified with --index), using | 
|  | 368 | the simple or xmlrpc index types (with --type xmlrpc / --type simple) | 
|  | 369 | """ | 
| Tarek Ziade | e265597 | 2011-05-31 12:15:42 +0200 | [diff] [blame^] | 370 | #opts = _parse_args(args[1:], '', ['simple', 'xmlrpc']) | 
| Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 371 | # 1. what kind of index is requested ? (xmlrpc / simple) | 
| Tarek Ziade | e265597 | 2011-05-31 12:15:42 +0200 | [diff] [blame^] | 372 | raise NotImplementedError() | 
| Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 373 |  | 
|  | 374 |  | 
|  | 375 | actions = [ | 
|  | 376 | ('run', 'Run one or several commands', _run), | 
|  | 377 | ('metadata', 'Display the metadata of a project', _metadata), | 
|  | 378 | ('install', 'Install a project', _install), | 
|  | 379 | ('remove', 'Remove a project', _remove), | 
|  | 380 | ('search', 'Search for a project in the indexes', _search), | 
|  | 381 | ('list', 'Search for local projects', _list), | 
|  | 382 | ('graph', 'Display a graph', _graph), | 
|  | 383 | ('create', 'Create a Project', _create), | 
|  | 384 | ] | 
|  | 385 |  | 
|  | 386 |  | 
|  | 387 | class Dispatcher: | 
|  | 388 | """Reads the command-line options | 
|  | 389 | """ | 
|  | 390 | def __init__(self, args=None): | 
|  | 391 | self.verbose = 1 | 
|  | 392 | self.dry_run = False | 
|  | 393 | self.help = False | 
|  | 394 | self.script_name = 'pysetup' | 
|  | 395 | self.cmdclass = {} | 
|  | 396 | self.commands = [] | 
|  | 397 | self.command_options = {} | 
|  | 398 |  | 
|  | 399 | for attr in display_option_names: | 
|  | 400 | setattr(self, attr, False) | 
|  | 401 |  | 
|  | 402 | self.parser = FancyGetopt(global_options + display_options) | 
|  | 403 | self.parser.set_negative_aliases(negative_opt) | 
|  | 404 | # FIXME this parses everything, including command options (e.g. "run | 
|  | 405 | # build -i" errors with "option -i not recognized") | 
|  | 406 | args = self.parser.getopt(args=args, object=self) | 
|  | 407 |  | 
|  | 408 | # if first arg is "run", we have some commands | 
|  | 409 | if len(args) == 0: | 
|  | 410 | self.action = None | 
|  | 411 | else: | 
|  | 412 | self.action = args[0] | 
|  | 413 |  | 
|  | 414 | allowed = [action[0] for action in actions] + [None] | 
|  | 415 | if self.action not in allowed: | 
|  | 416 | msg = 'Unrecognized action "%s"' % self.action | 
|  | 417 | raise PackagingArgError(msg) | 
|  | 418 |  | 
| Tarek Ziade | b1b6e13 | 2011-05-30 12:07:49 +0200 | [diff] [blame] | 419 | self._set_logger() | 
| Tarek Ziade | b1b6e13 | 2011-05-30 12:07:49 +0200 | [diff] [blame] | 420 | self.args = args | 
|  | 421 |  | 
| Tarek Ziade | e265597 | 2011-05-31 12:15:42 +0200 | [diff] [blame^] | 422 | # for display options we return immediately | 
| Tarek Ziade | b1b6e13 | 2011-05-30 12:07:49 +0200 | [diff] [blame] | 423 | if self.help or self.action is None: | 
|  | 424 | self._show_help(self.parser, display_options_=False) | 
|  | 425 |  | 
|  | 426 | def _set_logger(self): | 
| Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 427 | # setting up the logging level from the command-line options | 
|  | 428 | # -q gets warning, error and critical | 
|  | 429 | if self.verbose == 0: | 
|  | 430 | level = logging.WARNING | 
|  | 431 | # default level or -v gets info too | 
|  | 432 | # XXX there's a bug somewhere: the help text says that -v is default | 
|  | 433 | # (and verbose is set to 1 above), but when the user explicitly gives | 
|  | 434 | # -v on the command line, self.verbose is incremented to 2!  Here we | 
|  | 435 | # compensate for that (I tested manually).  On a related note, I think | 
|  | 436 | # it's a good thing to use -q/nothing/-v/-vv on the command line | 
|  | 437 | # instead of logging constants; it will be easy to add support for | 
|  | 438 | # logging configuration in setup.cfg for advanced users. --merwok | 
|  | 439 | elif self.verbose in (1, 2): | 
|  | 440 | level = logging.INFO | 
|  | 441 | else:  # -vv and more for debug | 
|  | 442 | level = logging.DEBUG | 
|  | 443 |  | 
| Tarek Ziade | b1b6e13 | 2011-05-30 12:07:49 +0200 | [diff] [blame] | 444 | # setting up the stream handler | 
|  | 445 | handler = logging.StreamHandler(sys.stderr) | 
|  | 446 | handler.setLevel(level) | 
|  | 447 | logger.addHandler(handler) | 
|  | 448 | logger.setLevel(level) | 
| Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 449 |  | 
|  | 450 | def _parse_command_opts(self, parser, args): | 
|  | 451 | # Pull the current command from the head of the command line | 
|  | 452 | command = args[0] | 
|  | 453 | if not command_re.match(command): | 
|  | 454 | raise SystemExit("invalid command name %r" % (command,)) | 
|  | 455 | self.commands.append(command) | 
|  | 456 |  | 
|  | 457 | # Dig up the command class that implements this command, so we | 
|  | 458 | # 1) know that it's a valid command, and 2) know which options | 
|  | 459 | # it takes. | 
|  | 460 | try: | 
|  | 461 | cmd_class = get_command_class(command) | 
|  | 462 | except PackagingModuleError as msg: | 
|  | 463 | raise PackagingArgError(msg) | 
|  | 464 |  | 
|  | 465 | # XXX We want to push this in packaging.command | 
|  | 466 | # | 
|  | 467 | # Require that the command class be derived from Command -- want | 
|  | 468 | # to be sure that the basic "command" interface is implemented. | 
|  | 469 | for meth in ('initialize_options', 'finalize_options', 'run'): | 
|  | 470 | if hasattr(cmd_class, meth): | 
|  | 471 | continue | 
|  | 472 | raise PackagingClassError( | 
|  | 473 | 'command %r must implement %r' % (cmd_class, meth)) | 
|  | 474 |  | 
|  | 475 | # Also make sure that the command object provides a list of its | 
|  | 476 | # known options. | 
|  | 477 | if not (hasattr(cmd_class, 'user_options') and | 
|  | 478 | isinstance(cmd_class.user_options, list)): | 
|  | 479 | raise PackagingClassError( | 
|  | 480 | "command class %s must provide " | 
|  | 481 | "'user_options' attribute (a list of tuples)" % cmd_class) | 
|  | 482 |  | 
|  | 483 | # If the command class has a list of negative alias options, | 
|  | 484 | # merge it in with the global negative aliases. | 
|  | 485 | _negative_opt = negative_opt.copy() | 
|  | 486 |  | 
|  | 487 | if hasattr(cmd_class, 'negative_opt'): | 
|  | 488 | _negative_opt.update(cmd_class.negative_opt) | 
|  | 489 |  | 
|  | 490 | # Check for help_options in command class.  They have a different | 
|  | 491 | # format (tuple of four) so we need to preprocess them here. | 
|  | 492 | if (hasattr(cmd_class, 'help_options') and | 
|  | 493 | isinstance(cmd_class.help_options, list)): | 
|  | 494 | help_options = cmd_class.help_options[:] | 
|  | 495 | else: | 
|  | 496 | help_options = [] | 
|  | 497 |  | 
|  | 498 | # All commands support the global options too, just by adding | 
|  | 499 | # in 'global_options'. | 
|  | 500 | parser.set_option_table(global_options + | 
|  | 501 | cmd_class.user_options + | 
|  | 502 | help_options) | 
|  | 503 | parser.set_negative_aliases(_negative_opt) | 
|  | 504 | args, opts = parser.getopt(args[1:]) | 
|  | 505 |  | 
|  | 506 | if hasattr(opts, 'help') and opts.help: | 
|  | 507 | self._show_command_help(cmd_class) | 
|  | 508 | return | 
|  | 509 |  | 
|  | 510 | if (hasattr(cmd_class, 'help_options') and | 
|  | 511 | isinstance(cmd_class.help_options, list)): | 
|  | 512 | help_option_found = False | 
|  | 513 | for help_option, short, desc, func in cmd_class.help_options: | 
|  | 514 | if hasattr(opts, help_option.replace('-', '_')): | 
|  | 515 | help_option_found = True | 
|  | 516 | if hasattr(func, '__call__'): | 
|  | 517 | func() | 
|  | 518 | else: | 
|  | 519 | raise PackagingClassError( | 
|  | 520 | "invalid help function %r for help option %r: " | 
|  | 521 | "must be a callable object (function, etc.)" | 
|  | 522 | % (func, help_option)) | 
|  | 523 |  | 
|  | 524 | if help_option_found: | 
|  | 525 | return | 
|  | 526 |  | 
|  | 527 | # Put the options from the command line into their official | 
|  | 528 | # holding pen, the 'command_options' dictionary. | 
|  | 529 | opt_dict = self.get_option_dict(command) | 
|  | 530 | for name, value in vars(opts).items(): | 
|  | 531 | opt_dict[name] = ("command line", value) | 
|  | 532 |  | 
|  | 533 | return args | 
|  | 534 |  | 
|  | 535 | def get_option_dict(self, command): | 
|  | 536 | """Get the option dictionary for a given command.  If that | 
|  | 537 | command's option dictionary hasn't been created yet, then create it | 
|  | 538 | and return the new dictionary; otherwise, return the existing | 
|  | 539 | option dictionary. | 
|  | 540 | """ | 
|  | 541 | d = self.command_options.get(command) | 
|  | 542 | if d is None: | 
|  | 543 | d = self.command_options[command] = {} | 
|  | 544 | return d | 
|  | 545 |  | 
|  | 546 | def show_help(self): | 
|  | 547 | self._show_help(self.parser) | 
|  | 548 |  | 
|  | 549 | def print_usage(self, parser): | 
|  | 550 | parser.set_option_table(global_options) | 
|  | 551 |  | 
|  | 552 | actions_ = ['    %s: %s' % (name, desc) for name, desc, __ in actions] | 
|  | 553 | usage = common_usage % {'actions': '\n'.join(actions_)} | 
|  | 554 |  | 
|  | 555 | parser.print_help(usage + "\nGlobal options:") | 
|  | 556 |  | 
|  | 557 | def _show_help(self, parser, global_options_=True, display_options_=True, | 
|  | 558 | commands=[]): | 
|  | 559 | # late import because of mutual dependence between these modules | 
|  | 560 | from packaging.command.cmd import Command | 
|  | 561 |  | 
|  | 562 | print('Usage: pysetup [options] action [action_options]') | 
|  | 563 | print('') | 
|  | 564 | if global_options_: | 
|  | 565 | self.print_usage(self.parser) | 
|  | 566 | print('') | 
|  | 567 |  | 
|  | 568 | if display_options_: | 
|  | 569 | parser.set_option_table(display_options) | 
|  | 570 | parser.print_help( | 
|  | 571 | "Information display options (just display " + | 
|  | 572 | "information, ignore any commands)") | 
|  | 573 | print('') | 
|  | 574 |  | 
|  | 575 | for command in commands: | 
|  | 576 | if isinstance(command, type) and issubclass(command, Command): | 
|  | 577 | cls = command | 
|  | 578 | else: | 
|  | 579 | cls = get_command_class(command) | 
|  | 580 | if (hasattr(cls, 'help_options') and | 
|  | 581 | isinstance(cls.help_options, list)): | 
|  | 582 | parser.set_option_table(cls.user_options + cls.help_options) | 
|  | 583 | else: | 
|  | 584 | parser.set_option_table(cls.user_options) | 
|  | 585 |  | 
|  | 586 | parser.print_help("Options for %r command:" % cls.__name__) | 
|  | 587 | print('') | 
|  | 588 |  | 
|  | 589 | def _show_command_help(self, command): | 
|  | 590 | if isinstance(command, str): | 
|  | 591 | command = get_command_class(command) | 
|  | 592 |  | 
| Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 593 | desc = getattr(command, 'description', '(no description available)') | 
|  | 594 | print('Description: %s' % desc) | 
|  | 595 | print('') | 
|  | 596 |  | 
|  | 597 | if (hasattr(command, 'help_options') and | 
|  | 598 | isinstance(command.help_options, list)): | 
|  | 599 | self.parser.set_option_table(command.user_options + | 
|  | 600 | command.help_options) | 
|  | 601 | else: | 
|  | 602 | self.parser.set_option_table(command.user_options) | 
|  | 603 |  | 
|  | 604 | self.parser.print_help("Options:") | 
|  | 605 | print('') | 
|  | 606 |  | 
|  | 607 | def _get_command_groups(self): | 
|  | 608 | """Helper function to retrieve all the command class names divided | 
|  | 609 | into standard commands (listed in | 
|  | 610 | packaging.command.STANDARD_COMMANDS) and extra commands (given in | 
|  | 611 | self.cmdclass and not standard commands). | 
|  | 612 | """ | 
|  | 613 | extra_commands = [cmd for cmd in self.cmdclass | 
|  | 614 | if cmd not in STANDARD_COMMANDS] | 
|  | 615 | return STANDARD_COMMANDS, extra_commands | 
|  | 616 |  | 
|  | 617 | def print_commands(self): | 
|  | 618 | """Print out a help message listing all available commands with a | 
|  | 619 | description of each.  The list is divided into standard commands | 
|  | 620 | (listed in packaging.command.STANDARD_COMMANDS) and extra commands | 
|  | 621 | (given in self.cmdclass and not standard commands).  The | 
|  | 622 | descriptions come from the command class attribute | 
|  | 623 | 'description'. | 
|  | 624 | """ | 
|  | 625 | std_commands, extra_commands = self._get_command_groups() | 
|  | 626 | max_length = max(len(command) | 
|  | 627 | for commands in (std_commands, extra_commands) | 
|  | 628 | for command in commands) | 
|  | 629 |  | 
|  | 630 | self.print_command_list(std_commands, "Standard commands", max_length) | 
|  | 631 | if extra_commands: | 
|  | 632 | print() | 
|  | 633 | self.print_command_list(extra_commands, "Extra commands", | 
|  | 634 | max_length) | 
|  | 635 |  | 
|  | 636 | def print_command_list(self, commands, header, max_length): | 
|  | 637 | """Print a subset of the list of all commands -- used by | 
|  | 638 | 'print_commands()'. | 
|  | 639 | """ | 
|  | 640 | print(header + ":") | 
|  | 641 |  | 
|  | 642 | for cmd in commands: | 
|  | 643 | cls = self.cmdclass.get(cmd) or get_command_class(cmd) | 
|  | 644 | description = getattr(cls, 'description', | 
|  | 645 | '(no description available)') | 
|  | 646 |  | 
|  | 647 | print("  %-*s  %s" % (max_length, cmd, description)) | 
|  | 648 |  | 
|  | 649 | def __call__(self): | 
|  | 650 | if self.action is None: | 
|  | 651 | return | 
|  | 652 | for action, desc, func in actions: | 
|  | 653 | if action == self.action: | 
|  | 654 | return func(self, self.args) | 
|  | 655 | return -1 | 
|  | 656 |  | 
|  | 657 |  | 
|  | 658 | def main(args=None): | 
| Tarek Ziade | b1b6e13 | 2011-05-30 12:07:49 +0200 | [diff] [blame] | 659 | old_level = logger.level | 
|  | 660 | old_handlers = copy(logger.handlers) | 
|  | 661 | try: | 
|  | 662 | dispatcher = Dispatcher(args) | 
|  | 663 | if dispatcher.action is None: | 
|  | 664 | return | 
|  | 665 | return dispatcher() | 
|  | 666 | finally: | 
|  | 667 | logger.setLevel(old_level) | 
|  | 668 | logger.handlers[:] = old_handlers | 
| Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 669 |  | 
| Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 670 |  | 
|  | 671 | if __name__ == '__main__': | 
|  | 672 | sys.exit(main()) |