diff --git a/pip/__init__.py b/pip/__init__.py index 99adb2a3860..f1b7c888b43 100755 --- a/pip/__init__.py +++ b/pip/__init__.py @@ -8,11 +8,13 @@ import difflib from pip.backwardcompat import walk_packages -from pip.basecommand import command_dict, load_command, load_all_commands, command_names -from pip.baseparser import parser from pip.exceptions import InstallationError from pip.log import logger from pip.util import get_installed_distributions +from pip.baseparser import create_main_parser +from pip.commands import commands, get_similar_commands, get_summaries +from pip.exceptions import CommandError, PipError +from pip.commands import commands, get_similar_commands, get_summaries def autocomplete(): @@ -30,8 +32,8 @@ def autocomplete(): current = cwords[cword-1] except IndexError: current = '' - load_all_commands() - subcommands = [cmd for cmd, cls in command_dict.items() if not cls.hidden] + + subcommands = [cmd for cmd, cls in commands.items() if not cls.hidden] options = [] # subcommand try: @@ -55,7 +57,7 @@ def autocomplete(): for dist in installed: print(dist) sys.exit(1) - subcommand = command_dict.get(subcommand_name) + subcommand = commands.get(subcommand_name) options += [(opt.get_opt_string(), opt.nargs) for opt in subcommand.parser.option_list if opt.help != optparse.SUPPRESS_HELP] @@ -71,11 +73,16 @@ def autocomplete(): opt_label += '=' print(opt_label) else: + parser = create_main_parser() + # show options of main parser only when necessary if current.startswith('-') or current.startswith('--'): - subcommands += [opt.get_opt_string() - for opt in parser.option_list - if opt.help != optparse.SUPPRESS_HELP] + opts = [i.option_list for i in parser.option_groups] + opts = (o for it in opts for o in it) #flatten without reduce + + subcommands += [i.get_opt_string() for i in opts + if i.help != optparse.SUPPRESS_HELP] + print(' '.join([x for x in subcommands if x.startswith(current)])) sys.exit(1) @@ -88,31 +95,63 @@ def version_control(): __import__(modname) +def parseopts(args): + parser = create_main_parser() + + # create command listing + command_summaries = get_summaries() + + description = ['Commands:'] + description.extend([' %-20s %s' % (i, j) for i,j in command_summaries]) + + parser.description = '\n'.join(description) + + options, args = parser.parse_args(args) + + if options.version: + sys.stdout.write(parser.version) + sys.stdout.write(os.linesep) + sys.exit() + + # pip || pip help || pip --help -> print_help() + if options.help or not args or (args[0] == 'help' and len(args) == 1): + parser.print_help() + sys.exit() + + if not args: + msg = 'You must give a command (use "pip --help" to see a list of commands)' + raise CommandError(msg) + + command = args[0].lower() + + if command not in commands: + guess = get_similar_commands(command) + + msg = ['unknown command "%s"' % command] + if guess: + msg.append('maybe you meant "%s"' % guess) + + raise CommandError(' - '.join(msg)) # TODO: + + return command, options, args, parser + + def main(initial_args=None): if initial_args is None: initial_args = sys.argv[1:] + autocomplete() version_control() - options, args = parser.parse_args(initial_args) - if options.help and not args: - args = ['help'] - if not args: - parser.error('You must give a command (use "pip help" to see a list of commands)') - command = args[0].lower() - load_command(command) - if command not in command_dict: - close_commands = difflib.get_close_matches(command, command_names()) - if close_commands: - guess = close_commands[0] - if args[1:]: - guess = "%s %s" % (guess, " ".join(args[1:])) - else: - guess = 'install %s' % command - error_dict = {'arg': command, 'guess': guess, - 'script': os.path.basename(sys.argv[0])} - parser.error('No command by the name %(script)s %(arg)s\n ' - '(maybe you meant "%(script)s %(guess)s")' % error_dict) - command = command_dict[command] + + try: + cmd_name, options, args, parser = parseopts(initial_args) + except PipError: + e = sys.exc_info()[1] + sys.stderr.write(str(e)) + sys.stderr.write(os.linesep) + sys.exit(1) + + command = commands[cmd_name](parser) #see baseparser.Command return command.main(args[1:], options) diff --git a/pip/basecommand.py b/pip/basecommand.py index 12bcd621108..c1d7af2dfb1 100644 --- a/pip/basecommand.py +++ b/pip/basecommand.py @@ -5,21 +5,18 @@ import sys import traceback import time +import optparse -from pip import commands from pip.log import logger -from pip.baseparser import parser, ConfigOptionParser, UpdatingDefaultsHelpFormatter from pip.download import urlopen +from pip.baseparser import ConfigOptionParser, UpdatingDefaultsHelpFormatter from pip.exceptions import (BadCommand, InstallationError, UninstallationError, CommandError) from pip.backwardcompat import StringIO, walk_packages from pip.status_codes import SUCCESS, ERROR, UNKNOWN_ERROR, VIRTUALENV_NOT_FOUND +__all__ = ['Command'] -__all__ = ['command_dict', 'Command', 'load_all_commands', - 'load_command', 'command_names'] - -command_dict = {} # for backwards compatibiliy get_proxy = urlopen.get_proxy @@ -30,29 +27,59 @@ class Command(object): usage = None hidden = False - def __init__(self): + def __init__(self, main_parser): assert self.name - self.parser = ConfigOptionParser( - usage=self.usage, - prog='%s %s' % (sys.argv[0], self.name), - version=parser.version, - formatter=UpdatingDefaultsHelpFormatter(), - name=self.name) - for option in parser.option_list: + + prog = os.path.basename(sys.argv[0]) + + parser_kw = { + 'usage' : self.usage, + 'prog' : '%s %s' % (prog, self.name), + 'add_help_option' : False, + 'formatter' : UpdatingDefaultsHelpFormatter(), + 'name' : self.name, } + + self.main_parser = main_parser + self.parser = ConfigOptionParser(**parser_kw) + + # Each command should populate this option group with the options + # that are specific to it. + self.command_group = optparse.OptionGroup(self.parser, 'Command options') + + # Re-add all options and option groups. This is quite lame :\ + # Copies all general options from the main parser. + for group in main_parser.option_groups: + self._copy_option_group(self.parser, group) + + self._copy_options(self.parser, main_parser.option_list) + + def _copy_options(self, parser, options): + """Populate an option parser or group with options.""" + for option in options: if not option.dest or option.dest == 'help': - # -h, --version, etc continue - self.parser.add_option(option) - command_dict[self.name] = self + parser.add_option(option) + + def _copy_option_group(self, parser, group): + """Copy option group (including options) to another parser.""" + new_group = optparse.OptionGroup(parser, group.title) + self._copy_options(new_group, group.option_list) + + parser.add_option_group(new_group) def merge_options(self, initial_options, options): # Make sure we have all global options carried over - for attr in ['log', 'proxy', 'require_venv', - 'log_explicit_levels', 'log_file', - 'timeout', 'default_vcs', - 'skip_requirements_regex', - 'no_input', 'exists_action']: - setattr(options, attr, getattr(initial_options, attr) or getattr(options, attr)) + + opts = ('log', 'proxy', 'require_venv', + 'log_explicit_levels', 'log_file', + 'timeout', 'default_vcs', + 'skip_requirements_regex', + 'no_input', 'exists_action') + + for attr in opts: + val = getattr(initial_options, attr) or getattr(options, attr) + setattr(options, attr, val) + options.quiet += initial_options.quiet options.verbose += initial_options.verbose @@ -170,24 +197,3 @@ def open_logfile(filename, mode='a'): log_fp.write('%s\n' % ('-'*60)) log_fp.write('%s run on %s\n' % (sys.argv[0], time.strftime('%c'))) return log_fp - - -def load_command(name): - full_name = 'pip.commands.%s' % name - if full_name in sys.modules: - return - try: - __import__(full_name) - except ImportError: - pass - - -def load_all_commands(): - for name in command_names(): - load_command(name) - - -def command_names(): - names = set((pkg[1] for pkg in walk_packages(path=commands.__path__))) - return list(names) - diff --git a/pip/baseparser.py b/pip/baseparser.py index 0742ef6f67f..2ce5d4c641e 100644 --- a/pip/baseparser.py +++ b/pip/baseparser.py @@ -179,6 +179,7 @@ def get_default_values(self): defaults[option.dest] = option.check_value(opt_str, default) return optparse.Values(defaults) + try: pip_dist = pkg_resources.get_distribution('pip') version = '%s from %s (python %s)' % ( @@ -187,108 +188,133 @@ def get_default_values(self): # when running pip.py without installing version=None -parser = ConfigOptionParser( - usage='%prog COMMAND [OPTIONS]', - version=version, - add_help_option=False, - formatter=UpdatingDefaultsHelpFormatter(), - name='global') - -parser.add_option( - '-h', '--help', - dest='help', - action='store_true', - help='Show help') -parser.add_option( + +def create_main_parser(): + from textwrap import dedent + + epilog = ''' + Further help: + - http://www.pip-installer.org/en/latest/index.html + ''' + + parser_kw = { + 'usage' : '%prog [options]', + 'add_help_option' : False, + 'formatter' : UpdatingDefaultsHelpFormatter(), + 'name' : 'global', + 'epilog' : dedent(epilog), + } + + parser = ConfigOptionParser(**parser_kw) + parser.disable_interspersed_args() + + # having a default version action just causes trouble + parser.version = version + + general_opts = optparse.OptionGroup(parser, 'General options') + padd = parser.add_option + gadd = general_opts.add_option + + # populate the main parser and the 'general options' group + gadd( '-h', '--help', + dest='help', + action='store_true', + help='Show help' ) + # Run only if inside a virtualenv, bail if not. - '--require-virtualenv', '--require-venv', - dest='require_venv', - action='store_true', - default=False, - help=optparse.SUPPRESS_HELP) - -parser.add_option( - '-v', '--verbose', - dest='verbose', - action='count', - default=0, - help='Give more output') -parser.add_option( - '-q', '--quiet', - dest='quiet', - action='count', - default=0, - help='Give less output') -parser.add_option( - '--log', - dest='log', - metavar='FILENAME', - help='Log file where a complete (maximum verbosity) record will be kept') -parser.add_option( + padd( '--require-virtualenv', '--require-venv', + dest='require_venv', + action='store_true', + default=False, + help=optparse.SUPPRESS_HELP) + + gadd( '-v', '--verbose', + dest='verbose', + action='count', + default=0, + help='Give more output') + + gadd( '-V', '--version', + dest='version', + action='store_true', + help='show version and exit') + + gadd( '-q', '--quiet', + dest='quiet', + action='count', + default=0, + help='Give less output') + + gadd( '--log', + dest='log', + metavar='FILENAME', + help='Log file where a complete (maximum verbosity) record will be kept') + # Writes the log levels explicitely to the log' - '--log-explicit-levels', - dest='log_explicit_levels', - action='store_true', - default=False, - help=optparse.SUPPRESS_HELP) -parser.add_option( + padd( '--log-explicit-levels', + dest='log_explicit_levels', + action='store_true', + default=False, + help=optparse.SUPPRESS_HELP) + # The default log file - '--local-log', '--log-file', - dest='log_file', - metavar='FILENAME', - default=default_log_file, - help=optparse.SUPPRESS_HELP) -parser.add_option( + padd( '--local-log', '--log-file', + dest='log_file', + metavar='FILENAME', + default=default_log_file, + help=optparse.SUPPRESS_HELP) + # Don't ask for input - '--no-input', - dest='no_input', - action='store_true', - default=False, - help=optparse.SUPPRESS_HELP) - -parser.add_option( - '--proxy', - dest='proxy', - type='str', - default='', - help="Specify a proxy in the form user:passwd@proxy.server:port. " - "Note that the user:password@ is optional and required only if you " - "are behind an authenticated proxy. If you provide " - "user@proxy.server:port then you will be prompted for a password.") -parser.add_option( - '--timeout', '--default-timeout', - metavar='SECONDS', - dest='timeout', - type='float', - default=15, - help='Set the socket timeout (default %default seconds)') -parser.add_option( + padd( '--no-input', + dest='no_input', + action='store_true', + default=False, + help=optparse.SUPPRESS_HELP) + + gadd( '--proxy', + dest='proxy', + type='str', + default='', + help="Specify a proxy in the form user:passwd@proxy.server:port. " + "Note that the user:password@ is optional and required only if you " + "are behind an authenticated proxy. If you provide " + "user@proxy.server:port then you will be prompted for a password.") + + gadd( '--timeout', '--default-timeout', + metavar='SECONDS', + dest='timeout', + type='float', + default=15, + help='Set the socket timeout (default %default seconds)') + # The default version control system for editables, e.g. 'svn' - '--default-vcs', - dest='default_vcs', - type='str', - default='', - help=optparse.SUPPRESS_HELP) -parser.add_option( + padd( '--default-vcs', + dest='default_vcs', + type='str', + default='', + help=optparse.SUPPRESS_HELP) + # A regex to be used to skip requirements - '--skip-requirements-regex', - dest='skip_requirements_regex', - type='str', - default='', - help=optparse.SUPPRESS_HELP) + padd( '--skip-requirements-regex', + dest='skip_requirements_regex', + type='str', + default='', + help=optparse.SUPPRESS_HELP) -parser.add_option( # Option when path already exist - '--exists-action', - dest='exists_action', - type='choice', - choices=['s', 'i', 'w', 'b'], - default=[], - action='append', - help="Default action when a path already exists." - "Use this option more then one time to specify " - "another action if a certain option is not " - "available, choices: " - "(s)witch, (i)gnore, (w)ipe, (b)ackup") - -parser.disable_interspersed_args() + gadd( '--exists-action', + dest='exists_action', + type='choice', + choices=['s', 'i', 'w', 'b'], + default=[], + action='append', + help="Default action when a path already exists." + "Use this option more then one time to specify " + "another action if a certain option is not " + "available, choices: " + "(s)witch, (i)gnore, (w)ipe, (b)ackup") + + + parser.add_option_group(general_opts) + return parser + diff --git a/pip/commands/__init__.py b/pip/commands/__init__.py index 792d6005489..b44a5a4016c 100644 --- a/pip/commands/__init__.py +++ b/pip/commands/__init__.py @@ -1 +1,56 @@ -# + +""" +Package containing all pip commands +""" + + +from pip.commands.bundle import BundleCommand +from pip.commands.completion import CompletionCommand +from pip.commands.freeze import FreezeCommand +from pip.commands.help import HelpCommand +from pip.commands.search import SearchCommand +from pip.commands.install import InstallCommand +from pip.commands.uninstall import UninstallCommand +from pip.commands.unzip import UnzipCommand +from pip.commands.zip import ZipCommand + + +commands = { + BundleCommand.name : BundleCommand, + CompletionCommand.name : CompletionCommand, + FreezeCommand.name : FreezeCommand, + HelpCommand.name : HelpCommand, + SearchCommand.name : SearchCommand, + InstallCommand.name : InstallCommand, + UninstallCommand.name : UninstallCommand, + UnzipCommand.name : UnzipCommand, + ZipCommand.name : ZipCommand, +} + + +def get_summaries(ignore_hidden=True): + """Return a sorted list of (command name, command summary).""" + items = [] + + for name, command_class in commands.items(): + if ignore_hidden and command_class.hidden: + continue + + items.append( (name, command_class.summary) ) + + return sorted(items) + + +def get_similar_commands(name): + """Command name auto-correct.""" + from difflib import get_close_matches + + close_commands = get_close_matches(name, commands.keys()) + + if close_commands: + guess = close_commands[0] + else: + guess = False + + return guess + diff --git a/pip/commands/bundle.py b/pip/commands/bundle.py index f782f1bc315..8bc88f4563c 100644 --- a/pip/commands/bundle.py +++ b/pip/commands/bundle.py @@ -11,13 +11,15 @@ class BundleCommand(InstallCommand): summary = 'Create pybundles (archives containing multiple packages)' bundle = True - def __init__(self): - super(BundleCommand, self).__init__() + def __init__(self, *args, **kw): + super(BundleCommand, self).__init__(*args, **kw) + # bundle uses different default source and build dirs build_opt = self.parser.get_option("--build") build_opt.default = backup_dir(build_prefix, '-bundle') src_opt = self.parser.get_option("--src") src_opt.default = backup_dir(src_prefix, '-bundle') + self.parser.set_defaults(**{ src_opt.dest: src_opt.default, build_opt.dest: build_opt.default, @@ -33,6 +35,3 @@ def run(self, options, args): self.bundle_filename = args.pop(0) requirement_set = super(BundleCommand, self).run(options, args) return requirement_set - - -BundleCommand() diff --git a/pip/commands/completion.py b/pip/commands/completion.py index 5b93d9cefe9..0d1f007c83c 100644 --- a/pip/commands/completion.py +++ b/pip/commands/completion.py @@ -32,20 +32,23 @@ class CompletionCommand(Command): summary = 'A helper command to be used for command completion' hidden = True - def __init__(self): - super(CompletionCommand, self).__init__() - self.parser.add_option( - '--bash', '-b', - action='store_const', - const='bash', - dest='shell', - help='Emit completion code for bash') - self.parser.add_option( - '--zsh', '-z', - action='store_const', - const='zsh', - dest='shell', - help='Emit completion code for zsh') + def __init__(self, *args, **kw): + super(CompletionCommand, self).__init__(*args, **kw) + gadd = self.command_group.add_option + + gadd( '--bash', '-b', + action='store_const', + const='bash', + dest='shell', + help='Emit completion code for bash') + + gadd( '--zsh', '-z', + action='store_const', + const='zsh', + dest='shell', + help='Emit completion code for zsh') + + self.parser.add_option_group(self.command_group) def run(self, options, args): """Prints the completion code of the given shell""" @@ -56,5 +59,3 @@ def run(self, options, args): print(BASE_COMPLETION % {'script': script, 'shell': options.shell}) else: sys.stderr.write('ERROR: You must pass %s\n' % ' or '.join(shell_options)) - -CompletionCommand() diff --git a/pip/commands/freeze.py b/pip/commands/freeze.py index 03ac80f55d1..4ecc40cbe3d 100644 --- a/pip/commands/freeze.py +++ b/pip/commands/freeze.py @@ -13,28 +13,31 @@ class FreezeCommand(Command): usage = '%prog [OPTIONS]' summary = 'Output all currently installed packages (exact versions) to stdout' - def __init__(self): - super(FreezeCommand, self).__init__() - self.parser.add_option( - '-r', '--requirement', - dest='requirement', - action='store', - default=None, - metavar='FILENAME', - help='Use the given requirements file as a hint about how to generate the new frozen requirements') - self.parser.add_option( - '-f', '--find-links', - dest='find_links', - action='append', - default=[], - metavar='URL', - help='URL for finding packages, which will be added to the frozen requirements file') - self.parser.add_option( - '-l', '--local', - dest='local', - action='store_true', - default=False, - help='If in a virtualenv, do not report globally-installed packages') + def __init__(self, *args, **kw): + super(FreezeCommand, self).__init__(*args, **kw) + gadd = self.command_group.add_option + + gadd( '-r', '--requirement', + dest='requirement', + action='store', + default=None, + metavar='FILENAME', + help='Use the given requirements file as a hint about how to generate the new frozen requirements') + + gadd( '-f', '--find-links', + dest='find_links', + action='append', + default=[], + metavar='URL', + help='URL for finding packages, which will be added to the frozen requirements file') + + gadd( '-l', '--local', + dest='local', + action='store_true', + default=False, + help='If in a virtualenv, do not report globally-installed packages') + + self.parser.add_option_group(self.command_group) def setup_logging(self): logger.move_stdout_to_stderr() @@ -106,6 +109,3 @@ def run(self, options, args): f.write('## The following requirements were added by pip --freeze:\n') for installation in sorted(installations.values(), key=lambda x: x.name): f.write(str(installation)) - - -FreezeCommand() diff --git a/pip/commands/help.py b/pip/commands/help.py index 4d504c521bb..b1695e4a395 100644 --- a/pip/commands/help.py +++ b/pip/commands/help.py @@ -1,8 +1,5 @@ -from pip.basecommand import (Command, command_dict, - load_all_commands, SUCCESS, - ERROR) +from pip.basecommand import Command, SUCCESS, ERROR from pip.exceptions import CommandError -from pip.baseparser import parser class HelpCommand(Command): @@ -11,23 +8,19 @@ class HelpCommand(Command): summary = 'Show available commands' def run(self, options, args): - load_all_commands() - if args: - ## FIXME: handle errors better here - command = args[0] - if command not in command_dict: - raise CommandError('No command with the name: %s' % command) - command = command_dict[command] - command.parser.print_help() + from pip.commands import commands + + try: + # 'pip help' with no args is handled by pip.__init__.parseopt() + cmd_name = args[0] # the command we need help for + except: return SUCCESS - parser.print_help() - print('\nCommands available:') - commands = list(set(command_dict.values())) - commands.sort(key=lambda x: x.name) - for command in commands: - if command.hidden: - continue - print(' %s: %s' % (command.name, command.summary)) - return SUCCESS -HelpCommand() + if cmd_name not in commands: + raise CommandError('unknown command "%s"' % cmd_name) + + # uhm, passing self.main_parser is a no no ; fix later + command = commands[cmd_name](self.main_parser) # instantiate + command.parser.print_help() + + return SUCCESS diff --git a/pip/commands/install.py b/pip/commands/install.py index 925d57feb4a..cb698bf5001 100644 --- a/pip/commands/install.py +++ b/pip/commands/install.py @@ -17,153 +17,151 @@ class InstallCommand(Command): summary = 'Install packages' bundle = False - def __init__(self): - super(InstallCommand, self).__init__() - self.parser.add_option( - '-e', '--editable', - dest='editables', - action='append', - default=[], - metavar='VCS+REPOS_URL[@REV]#egg=PACKAGE', - help='Install a package directly from a checkout. Source will be checked ' - 'out into src/PACKAGE (lower-case) and installed in-place (using ' - 'setup.py develop). You can run this on an existing directory/checkout (like ' - 'pip install -e src/mycheckout). This option may be provided multiple times. ' - 'Possible values for VCS are: svn, git, hg and bzr.') - self.parser.add_option( - '-r', '--requirement', - dest='requirements', - action='append', - default=[], - metavar='FILENAME', - help='Install all the packages listed in the given requirements file. ' - 'This option can be used multiple times.') - self.parser.add_option( - '-f', '--find-links', - dest='find_links', - action='append', - default=[], - metavar='URL', - help='URL to look for packages at') - self.parser.add_option( - '-i', '--index-url', '--pypi-url', - dest='index_url', - metavar='URL', - default='http://pypi.python.org/simple/', - help='Base URL of Python Package Index (default %default)') - self.parser.add_option( - '--extra-index-url', - dest='extra_index_urls', - metavar='URL', - action='append', - default=[], - help='Extra URLs of package indexes to use in addition to --index-url') - self.parser.add_option( - '--no-index', - dest='no_index', - action='store_true', - default=False, - help='Ignore package index (only looking at --find-links URLs instead)') - self.parser.add_option( - '-M', '--use-mirrors', - dest='use_mirrors', - action='store_true', - default=False, - help='Use the PyPI mirrors as a fallback in case the main index is down.') - self.parser.add_option( - '--mirrors', - dest='mirrors', - metavar='URL', - action='append', - default=[], - help='Specific mirror URLs to query when --use-mirrors is used') - - self.parser.add_option( - '-b', '--build', '--build-dir', '--build-directory', - dest='build_dir', - metavar='DIR', - default=build_prefix, - help='Unpack packages into DIR (default %default) and build from there') - self.parser.add_option( - '-t', '--target', - dest='target_dir', - metavar='DIR', - default=None, - help='Install packages into DIR.') - self.parser.add_option( - '-d', '--download', '--download-dir', '--download-directory', - dest='download_dir', - metavar='DIR', - default=None, - help='Download packages into DIR instead of installing them') - self.parser.add_option( - '--download-cache', - dest='download_cache', - metavar='DIR', - default=None, - help='Cache downloaded packages in DIR') - self.parser.add_option( - '--src', '--source', '--source-dir', '--source-directory', - dest='src_dir', - metavar='DIR', - default=src_prefix, - help='Check out --editable packages into DIR (default %default)') - - self.parser.add_option( - '-U', '--upgrade', - dest='upgrade', - action='store_true', - help='Upgrade all packages to the newest available version') - self.parser.add_option( - '--force-reinstall', - dest='force_reinstall', - action='store_true', - help='When upgrading, reinstall all packages even if they are ' - 'already up-to-date.') - self.parser.add_option( - '-I', '--ignore-installed', - dest='ignore_installed', - action='store_true', - help='Ignore the installed packages (reinstalling instead)') - self.parser.add_option( - '--no-deps', '--no-dependencies', - dest='ignore_dependencies', - action='store_true', - default=False, - help='Ignore package dependencies') - self.parser.add_option( - '--no-install', - dest='no_install', - action='store_true', - help="Download and unpack all packages, but don't actually install them") - self.parser.add_option( - '--no-download', - dest='no_download', - action="store_true", - help="Don't download any packages, just install the ones already downloaded " - "(completes an install run with --no-install)") - - self.parser.add_option( - '--install-option', - dest='install_options', - action='append', - help="Extra arguments to be supplied to the setup.py install " - "command (use like --install-option=\"--install-scripts=/usr/local/bin\"). " - "Use multiple --install-option options to pass multiple options to setup.py install. " - "If you are using an option with a directory path, be sure to use absolute path.") - - self.parser.add_option( - '--global-option', - dest='global_options', - action='append', - help="Extra global options to be supplied to the setup.py" - "call before the install command") - - self.parser.add_option( - '--user', - dest='use_user_site', - action='store_true', - help='Install to user-site') + def __init__(self, *args, **kw): + super(InstallCommand, self).__init__(*args, **kw) + gadd = self.command_group.add_option + + gadd( '-e', '--editable', + dest='editables', + action='append', + default=[], + metavar='VCS+REPOS_URL[@REV]#egg=PACKAGE', + help='Install a package directly from a checkout. Source will be checked ' + 'out into src/PACKAGE (lower-case) and installed in-place (using ' + 'setup.py develop). You can run this on an existing directory/checkout (like ' + 'pip install -e src/mycheckout). This option may be provided multiple times. ' + 'Possible values for VCS are: svn, git, hg and bzr.') + + gadd( '-r', '--requirement', + dest='requirements', + action='append', + default=[], + metavar='FILENAME', + help='Install all the packages listed in the given requirements file. ' + 'This option can be used multiple times.') + + gadd( '-f', '--find-links', + dest='find_links', + action='append', + default=[], + metavar='URL', + help='URL to look for packages at') + + gadd( '-i', '--index-url', '--pypi-url', + dest='index_url', + metavar='URL', + default='http://pypi.python.org/simple/', + help='Base URL of Python Package Index (default %default)') + + gadd( '--extra-index-url', + dest='extra_index_urls', + metavar='URL', + action='append', + default=[], + help='Extra URLs of package indexes to use in addition to --index-url') + + gadd( '--no-index', + dest='no_index', + action='store_true', + default=False, + help='Ignore package index (only looking at --find-links URLs instead)') + + gadd( '-M', '--use-mirrors', + dest='use_mirrors', + action='store_true', + default=False, + help='Use the PyPI mirrors as a fallback in case the main index is down.') + + gadd( '--mirrors', + dest='mirrors', + metavar='URL', + action='append', + default=[], + help='Specific mirror URLs to query when --use-mirrors is used') + + gadd( '-b', '--build', '--build-dir', '--build-directory', + dest='build_dir', + metavar='DIR', + default=build_prefix, + help='Unpack packages into DIR (default %default) and build from there') + + gadd( '-t', '--target', + dest='target_dir', + metavar='DIR', + default=None, + help='Install packages into DIR.') + + gadd( '-d', '--download', '--download-dir', '--download-directory', + dest='download_dir', + metavar='DIR', + default=None, + help='Download packages into DIR instead of installing them') + + gadd( '--download-cache', + dest='download_cache', + metavar='DIR', + default=None, + help='Cache downloaded packages in DIR') + + gadd( '--src', '--source', '--source-dir', '--source-directory', + dest='src_dir', + metavar='DIR', + default=src_prefix, + help='Check out --editable packages into DIR (default %default)') + + gadd( '-U', '--upgrade', + dest='upgrade', + action='store_true', + help='Upgrade all packages to the newest available version') + + gadd( '--force-reinstall', + dest='force_reinstall', + action='store_true', + help='When upgrading, reinstall all packages even if they are ' + 'already up-to-date.') + + gadd( '-I', '--ignore-installed', + dest='ignore_installed', + action='store_true', + help='Ignore the installed packages (reinstalling instead)') + + gadd( '--no-deps', '--no-dependencies', + dest='ignore_dependencies', + action='store_true', + default=False, + help='Ignore package dependencies') + + gadd( '--no-install', + dest='no_install', + action='store_true', + help="Download and unpack all packages, but don't actually install them") + + gadd( '--no-download', + dest='no_download', + action="store_true", + help="Don't download any packages, just install the ones already downloaded " + "(completes an install run with --no-install)") + + gadd( '--install-option', + dest='install_options', + action='append', + help="Extra arguments to be supplied to the setup.py install " + "command (use like --install-option=\"--install-scripts=/usr/local/bin\"). " + "Use multiple --install-option options to pass multiple options to setup.py install. " + "If you are using an option with a directory path, be sure to use absolute path.") + + gadd( '--global-option', + dest='global_options', + action='append', + help="Extra global options to be supplied to the setup.py" + "call before the install command") + + gadd( '--user', + dest='use_user_site', + action='store_true', + help='Install to user-site') + + self.parser.add_option_group(self.command_group) def _build_package_finder(self, options, index_urls): """ @@ -274,6 +272,3 @@ def run(self, options, args): ) shutil.rmtree(temp_target_dir) return requirement_set - - -InstallCommand() diff --git a/pip/commands/search.py b/pip/commands/search.py index 35eb4fc2a14..619411796e6 100644 --- a/pip/commands/search.py +++ b/pip/commands/search.py @@ -16,14 +16,17 @@ class SearchCommand(Command): usage = '%prog QUERY' summary = 'Search PyPI' - def __init__(self): - super(SearchCommand, self).__init__() - self.parser.add_option( - '--index', - dest='index', - metavar='URL', - default='http://pypi.python.org/pypi', - help='Base URL of Python Package Index (default %default)') + def __init__(self, *args, **kw): + super(SearchCommand, self).__init__(*args, **kw) + gadd = self.command_group.add_option + + gadd( '--index', + dest='index', + metavar='URL', + default='http://pypi.python.org/pypi', + help='Base URL of Python Package Index (default %default)') + + self.parser.add_option_group(self.command_group) def run(self, options, args): if not args: @@ -124,6 +127,3 @@ def compare_versions(version1, version2): def highest_version(versions): return reduce((lambda v1, v2: compare_versions(v1, v2) == 1 and v1 or v2), versions) - - -SearchCommand() diff --git a/pip/commands/uninstall.py b/pip/commands/uninstall.py index 9f2b891218f..fb35f4151cb 100644 --- a/pip/commands/uninstall.py +++ b/pip/commands/uninstall.py @@ -8,21 +8,24 @@ class UninstallCommand(Command): usage = '%prog [OPTIONS] PACKAGE_NAMES ...' summary = 'Uninstall packages' - def __init__(self): - super(UninstallCommand, self).__init__() - self.parser.add_option( - '-r', '--requirement', - dest='requirements', - action='append', - default=[], - metavar='FILENAME', - help='Uninstall all the packages listed in the given requirements file. ' - 'This option can be used multiple times.') - self.parser.add_option( - '-y', '--yes', - dest='yes', - action='store_true', - help="Don't ask for confirmation of uninstall deletions.") + def __init__(self, *args, **kw): + super(UninstallCommand, self).__init__(*args, **kw) + gadd = self.command_group.add_option + + gadd( '-r', '--requirement', + dest='requirements', + action='append', + default=[], + metavar='FILENAME', + help='Uninstall all the packages listed in the given requirements file. ' + 'This option can be used multiple times.') + + gadd( '-y', '--yes', + dest='yes', + action='store_true', + help="Don't ask for confirmation of uninstall deletions.") + + self.parser.add_option_group(self.command_group) def run(self, options, args): requirement_set = RequirementSet( @@ -39,5 +42,3 @@ def run(self, options, args): raise InstallationError('You must give at least one requirement ' 'to %(name)s (see "pip help %(name)s")' % dict(name=self.name)) requirement_set.uninstall(auto_confirm=options.yes) - -UninstallCommand() diff --git a/pip/commands/unzip.py b/pip/commands/unzip.py index f83e182059e..03c69398858 100644 --- a/pip/commands/unzip.py +++ b/pip/commands/unzip.py @@ -4,6 +4,3 @@ class UnzipCommand(ZipCommand): name = 'unzip' summary = 'Unzip individual packages' - - -UnzipCommand() diff --git a/pip/commands/zip.py b/pip/commands/zip.py index ebe1d791a4d..919a6d3e626 100644 --- a/pip/commands/zip.py +++ b/pip/commands/zip.py @@ -15,45 +15,47 @@ class ZipCommand(Command): usage = '%prog [OPTIONS] PACKAGE_NAMES...' summary = 'Zip individual packages' - def __init__(self): - super(ZipCommand, self).__init__() + def __init__(self, *args, **kw): + super(ZipCommand, self).__init__(*args, **kw) + gadd = self.command_group.add_option + if self.name == 'zip': - self.parser.add_option( - '--unzip', - action='store_true', - dest='unzip', - help='Unzip (rather than zip) a package') + gadd( '--unzip', + action='store_true', + dest='unzip', + help='Unzip (rather than zip) a package') else: - self.parser.add_option( - '--zip', - action='store_false', - dest='unzip', - default=True, - help='Zip (rather than unzip) a package') - self.parser.add_option( - '--no-pyc', - action='store_true', - dest='no_pyc', - help='Do not include .pyc files in zip files (useful on Google App Engine)') - self.parser.add_option( - '-l', '--list', - action='store_true', - dest='list', - help='List the packages available, and their zip status') - self.parser.add_option( - '--sort-files', - action='store_true', - dest='sort_files', - help='With --list, sort packages according to how many files they contain') - self.parser.add_option( - '--path', - action='append', - dest='paths', - help='Restrict operations to the given paths (may include wildcards)') - self.parser.add_option( - '-n', '--simulate', - action='store_true', - help='Do not actually perform the zip/unzip operation') + gadd( '--zip', + action='store_false', + dest='unzip', + default=True, + help='Zip (rather than unzip) a package') + + gadd( '--no-pyc', + action='store_true', + dest='no_pyc', + help='Do not include .pyc files in zip files (useful on Google App Engine)') + + gadd( '-l', '--list', + action='store_true', + dest='list', + help='List the packages available, and their zip status') + + gadd( '--sort-files', + action='store_true', + dest='sort_files', + help='With --list, sort packages according to how many files they contain') + + gadd( '--path', + action='append', + dest='paths', + help='Restrict operations to the given paths (may include wildcards)') + + gadd( '-n', '--simulate', + action='store_true', + help='Do not actually perform the zip/unzip operation') + + self.parser.add_option_group(self.command_group) def paths(self): """All the entries of sys.path, possibly restricted by --path""" @@ -342,5 +344,3 @@ def count_package(self, path): total += len(filenames) return total - -ZipCommand() diff --git a/tests/test_help.py b/tests/test_help.py index e638963e77a..dc7bed0720f 100644 --- a/tests/test_help.py +++ b/tests/test_help.py @@ -2,6 +2,7 @@ from pip.commands.help import (HelpCommand, SUCCESS, ERROR,) +from pip.baseparser import create_main_parser from mock import Mock from nose.tools import assert_raises from tests.test_pip import run_pip, reset_env @@ -13,7 +14,7 @@ def test_run_method_should_return_sucess_when_finds_command_name(): """ options_mock = Mock() args = ('freeze',) - help_cmd = HelpCommand() + help_cmd = HelpCommand(create_main_parser()) status = help_cmd.run(options_mock, args) assert status == SUCCESS @@ -22,9 +23,11 @@ def test_run_method_should_return_sucess_when_command_name_not_specified(): """ Test HelpCommand.run when there are no args """ + + # 'pip help' with no args is handled by pip.__init__.parseopt() options_mock = Mock() args = () - help_cmd = HelpCommand() + help_cmd = HelpCommand(create_main_parser()) status = help_cmd.run(options_mock, args) assert status == SUCCESS @@ -35,7 +38,7 @@ def test_run_method_should_raise_command_error_when_command_does_not_exist(): """ options_mock = Mock() args = ('mycommand',) - help_cmd = HelpCommand() + help_cmd = HelpCommand(create_main_parser()) assert_raises(CommandError, help_cmd.run, options_mock, args) @@ -57,6 +60,22 @@ def test_help_command_should_exit_status_ok_when_no_command_is_specified(): assert result.returncode == SUCCESS +def test_help_commands_equally_functional(): + """ + Test for `pip help`, `pip` and `pip --help` being the same + """ + reset_env() + + results = list(map(run_pip, ('help', '--help'))) + results.append(run_pip()) + + out = map(lambda x: x.stdout, results) + ret = map(lambda x: x.returncode, results) + + assert len(set(out)) == 1 + assert sum(ret) == 0 + + def test_help_command_should_exit_status_error_when_command_does_not_exist(): """ Test `help` command for non-existing command diff --git a/tests/test_search.py b/tests/test_search.py index 75338758f86..63b4b8a4a3f 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -4,6 +4,7 @@ transform_hits, SearchCommand) from pip.status_codes import NO_MATCHES_FOUND, SUCCESS +from pip.baseparser import create_main_parser from pip.backwardcompat import xmlrpclib, b from mock import Mock from tests.test_pip import run_pip, reset_env, pyversion @@ -85,7 +86,7 @@ def test_searching_through_Search_class(): dumped_xmlrpc_request = b(xmlrpclib.dumps(({'name': query, 'summary': query}, 'or'), 'search')) expected = [{'_pypi_ordering': 100, 'name': 'foo', 'summary': 'foo summary', 'version': '1.0'}] fake_transport.request.return_value = (expected,) - pypi_searcher = SearchCommand() + pypi_searcher = SearchCommand(create_main_parser()) result = pypi_searcher.search(query, 'http://pypi.python.org/pypi') try: assert expected == result, result @@ -109,7 +110,7 @@ def test_run_method_should_return_sucess_when_find_packages(): """ options_mock = Mock() options_mock.index = 'http://pypi.python.org/pypi' - search_cmd = SearchCommand() + search_cmd = SearchCommand(create_main_parser()) status = search_cmd.run(options_mock, ('pip',)) assert status == SUCCESS @@ -120,7 +121,7 @@ def test_run_method_should_return_no_matches_found_when_does_not_find_packages() """ options_mock = Mock() options_mock.index = 'http://pypi.python.org/pypi' - search_cmd = SearchCommand() + search_cmd = SearchCommand(create_main_parser()) status = search_cmd.run(options_mock, ('non-existant-package',)) assert status == NO_MATCHES_FOUND, status