From ddff62df370cab37726a72ccfc13b08d5118f680 Mon Sep 17 00:00:00 2001 From: "maruel@chromium.org" Date: Mon, 17 May 2010 21:02:36 +0000 Subject: [PATCH] Goes further down the rabbit hole. Enable automatic command and one-liner doc. Reformat pydoc accordingly. Add parser as an argument and parse_args hook in preparation to move parse_args at the right place, inside the CMDxx functions. Update unit tests Review URL: http://codereview.chromium.org/2129005 git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@47449 0039d316-1c4b-4281-b951-d872f2087c98 --- gclient.py | 273 +++++++++++++++++++----------------------- tests/gclient_test.py | 20 ++-- 2 files changed, 134 insertions(+), 159 deletions(-) diff --git a/gclient.py b/gclient.py index d3d7c11c4..e7672a14b 100644 --- a/gclient.py +++ b/gclient.py @@ -55,7 +55,7 @@ Hooks ] """ -__version__ = "0.3.5" +__version__ = "0.3.7" import errno import logging @@ -75,23 +75,10 @@ from third_party.repo.progress import Progress # default help text DEFAULT_USAGE_TEXT = ( -"""usage: %prog [options] [--] [SCM options/args...] +"""%prog [options] [--] [SCM options/args...] a wrapper for managing a set of svn client modules and/or git repositories. Version """ + __version__ + """ -subcommands: - cleanup - config - diff - export - pack - revert - status - sync - update - runhooks - revinfo - Options and extra arguments can be passed to invoked SCM commands by appending them to the command line. Note that if the first such appended option starts with a dash (-) then the options must be @@ -829,8 +816,9 @@ solutions = [ ## gclient commands. -def CMDcleanup(options, args): +def CMDcleanup(parser, options, args): """Clean up all working copies, using 'svn cleanup' for each module. + Additional options and args may be passed to 'svn cleanup'. usage: cleanup [options] [--] [svn cleanup args/options] @@ -848,9 +836,10 @@ Valid options: return client.RunOnDeps('cleanup', args) -def CMDconfig(options, args): - """Create a .gclient file in the current directory; this -specifies the configuration for further commands. After update/sync, +def CMDconfig(parser, options, args): + """Create a .gclient file in the current directory. + +This specifies the configuration for further commands. After update/sync, top-level DEPS files in each module are read to determine dependent modules to operate on as well. If optional [url] parameter is provided, then configuration is read from a specified Subversion server @@ -897,9 +886,8 @@ Examples: return 0 -def CMDexport(options, args): - """Wrapper for svn export for all managed directories -""" +def CMDexport(parser, options, args): + """Wrapper for svn export for all managed directories.""" if len(args) != 1: raise gclient_utils.Error("Need directory name") client = GClient.LoadCurrentConfig(options) @@ -914,27 +902,9 @@ def CMDexport(options, args): return client.RunOnDeps('export', args) -def CMDhelp(options, args): - """Describe the usage of this program or its subcommands. - -usage: help [options] [subcommand] - -Valid options: - --verbose : output additional diagnostics -""" - __pychecker__ = 'unusednames=options' - module = sys.modules[__name__] - commands = [x[3:] for x in dir(module) if x.startswith('CMD')] - if len(args) == 1 and args[0] in commands: - print getattr(module, 'CMD' + args[0]).__doc__ - else: - raise gclient_utils.Error("unknown subcommand '%s'; see 'gclient help'" % - args[0]) - return 0 - - -def CMDpack(options, args): +def CMDpack(parser, options, args): """Generate a patch which can be applied at the root of the tree. + Internally, runs 'svn diff' on each checked out module and dependencies, and performs minimal postprocessing of the output. The resulting patch is printed to stdout and can be applied to a freshly @@ -966,9 +936,10 @@ Examples: return client.RunOnDeps('pack', args) -def CMDstatus(options, args): - """Show the status of client and dependent modules, using 'svn diff' -for each module. Additional options and args may be passed to 'svn diff'. +def CMDstatus(parser, options, args): + """Show the modification status of for every dependencies. + +Additional options and args may be passed to 'svn status'. usage: status [options] [--] [svn diff args/options] @@ -986,17 +957,17 @@ Valid options: return client.RunOnDeps('status', args) -def CMDsync(options, args): - """Perform a checkout/update of the modules specified by the gclient -configuration; see 'help config'. Unless --revision is specified, -then the latest revision of the root solutions is checked out, with -dependent submodule versions updated according to DEPS files. -If --revision is specified, then the given revision is used in place +def CMDsync(parser, options, args): + """Checkout/update the modules specified by the gclient configuration. + +Unless --revision is specified, then the latest revision of the root solutions +is checked out, with dependent submodule versions updated according to DEPS +files. If --revision is specified, then the given revision is used in place of the latest, either for a single solution or for all solutions. Unless the --force option is provided, solutions and modules whose local revision matches the one to update (i.e., they have not changed in the repository) are *not* modified. Unless --nohooks is provided, -the hooks are run. +the hooks are run. See 'help config' for more information. usage: gclient sync [options] [--] [SCM update options/args] @@ -1052,14 +1023,14 @@ Examples: return client.RunOnDeps('update', args) -def CMDupdate(options, args): - """Alias for the sync command. Deprecated. -""" - return CMDsync(options, args) +def CMDupdate(parser, options, args): + """Alias for the sync command. Deprecated.""" + return CMDsync(parser, options, args) -def CMDdiff(options, args): +def CMDdiff(parser, options, args): """Display the differences between two revisions of modules. + (Does 'svn diff' for each checked out module and dependences.) Additional args and options to 'svn diff' can be passed after gclient options. @@ -1087,18 +1058,18 @@ Examples: return client.RunOnDeps('diff', args) -def CMDrevert(options, args): - """Revert every file in every managed directory in the client view. -""" +def CMDrevert(parser, options, args): + """Revert every file in every managed directory in the client view.""" client = GClient.LoadCurrentConfig(options) if not client: raise gclient_utils.Error("client not configured; see 'gclient config'") return client.RunOnDeps('revert', args) -def CMDrunhooks(options, args): - """Runs hooks for files that have been modified in the local working copy, -according to 'svn status'. Implies --force. +def CMDrunhooks(parser, options, args): + """Runs hooks for files that have been modified in the local working copy. + +Implies --force. usage: runhooks [options] @@ -1116,8 +1087,10 @@ Valid options: return client.RunOnDeps('runhooks', args) -def CMDrevinfo(options, args): - """Outputs source path, server URL and revision information for every +def CMDrevinfo(parser, options, args): + """Outputs defails for every dependencies. + +This includes source path, server URL and revision information for every dependency in all solutions. usage: revinfo [options] @@ -1130,102 +1103,102 @@ usage: revinfo [options] return 0 -def DispatchCommand(command, options, args): - """Dispatches the appropriate subcommand based on command line arguments. -""" - module = sys.modules[__name__] - command = getattr(module, 'CMD' + command, None) - if command: - return command(options, args) - else: - raise gclient_utils.Error("unknown subcommand '%s'; see 'gclient help'" % - command) +def CMDhelp(parser, options, args): + """Prints general help or command-specific documentation.""" + if len(args) == 1: + command = Command(args[0]) + if command: + print getattr(sys.modules[__name__], 'CMD' + args[0]).__doc__ + return 0 + parser.usage = (DEFAULT_USAGE_TEXT + '\nCommands are:\n' + '\n'.join([ + ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip()) + for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')])) + parser.print_help() + return 0 + + +def Command(command): + return getattr(sys.modules[__name__], 'CMD' + command, CMDhelp) def Main(argv): - option_parser = optparse.OptionParser(usage=DEFAULT_USAGE_TEXT, - version=__version__) - option_parser.add_option("--force", action="store_true", - help="(update/sync only) force update even " - "for modules which haven't changed") - option_parser.add_option("--nohooks", action="store_true", - help="(update/sync/revert only) prevent the hooks " - "from running") - option_parser.add_option("--revision", action="append", dest="revisions", - metavar="REV", default=[], - help="(update/sync only) sync to a specific " - "revision, can be used multiple times for " - "each solution, e.g. --revision=src@123, " - "--revision=internal@32") - option_parser.add_option("--deps", dest="deps_os", metavar="OS_LIST", - help="(update/sync only) sync deps for the " - "specified (comma-separated) platform(s); " - "'all' will sync all platforms") - option_parser.add_option("--reset", action="store_true", - help="(update/sync only) resets any local changes " - "before updating (git only)") - option_parser.add_option("--spec", - help="(config only) create a gclient file " - "containing the provided string") - option_parser.add_option("-v", "--verbose", action="count", default=0, - help="produce additional output for diagnostics") - option_parser.add_option("--manually_grab_svn_rev", action="store_true", - help="Skip svn up whenever possible by requesting " - "actual HEAD revision from the repository") - option_parser.add_option("--head", action="store_true", - help="skips any safesync_urls specified in " - "configured solutions") - option_parser.add_option("--delete_unversioned_trees", action="store_true", - help="on update, delete any unexpected " - "unversioned trees that are in the checkout") - option_parser.add_option("--snapshot", action="store_true", - help="(revinfo only), create a snapshot file " - "of the current version of all repositories") - option_parser.add_option("--name", - help="specify alternate relative solution path") - option_parser.add_option("--gclientfile", metavar="FILENAME", - help="specify an alternate .gclient file") - - if len(argv) < 2: - # Users don't need to be told to use the 'help' command. - option_parser.print_help() - return 1 + parser = optparse.OptionParser(usage=DEFAULT_USAGE_TEXT, + version='%prog ' + __version__) + parser.add_option("-v", "--verbose", action="count", default=0, + help="Produces additional output for diagnostics. Can be " + "used up to three times for more logging info.") + parser.add_option("--gclientfile", metavar="FILENAME", dest="config_filename", + default=os.environ.get("GCLIENT_FILE", ".gclient"), + help="Specify an alternate .gclient file") + # The other options will be moved eventually. + parser.add_option("--force", action="store_true", + help="(update/sync only) force update even " + "for modules which haven't changed") + parser.add_option("--nohooks", action="store_true", + help="(update/sync/revert only) prevent the hooks " + "from running") + parser.add_option("--revision", action="append", dest="revisions", + metavar="REV", default=[], + help="(update/sync only) sync to a specific " + "revision, can be used multiple times for " + "each solution, e.g. --revision=src@123, " + "--revision=internal@32") + parser.add_option("--deps", dest="deps_os", metavar="OS_LIST", + help="(update/sync only) sync deps for the " + "specified (comma-separated) platform(s); " + "'all' will sync all platforms") + parser.add_option("--reset", action="store_true", + help="(update/sync only) resets any local changes " + "before updating (git only)") + parser.add_option("--spec", + help="(config only) create a gclient file " + "containing the provided string") + parser.add_option("--manually_grab_svn_rev", action="store_true", + help="Skip svn up whenever possible by requesting " + "actual HEAD revision from the repository") + parser.add_option("--head", action="store_true", + help="skips any safesync_urls specified in " + "configured solutions") + parser.add_option("--delete_unversioned_trees", action="store_true", + help="on update, delete any unexpected " + "unversioned trees that are in the checkout") + parser.add_option("--snapshot", action="store_true", + help="(revinfo only), create a snapshot file " + "of the current version of all repositories") + parser.add_option("--name", + help="specify alternate relative solution path") + # Integrate standard options processing. + old_parser = parser.parse_args + def Parse(args): + (options, args) = old_parser(args) + if options.verbose == 2: + logging.basicConfig(level=logging.INFO) + elif options.verbose > 2: + logging.basicConfig(level=logging.DEBUG) + options.entries_filename = options.config_filename + "_entries" + return (options, args) + parser.parse_args = Parse + # We don't want wordwrapping in epilog (usually examples) + parser.format_epilog = lambda _: parser.epilog or '' + + if not len(argv): + argv = ['help'] # Add manual support for --version as first argument. - if argv[1] == '--version': - option_parser.print_version() + if argv[0] == '--version': + parser.print_version() return 0 - # Add manual support for --help as first argument. - if argv[1] == '--help': - argv[1] = 'help' - - command = argv[1] - options, args = option_parser.parse_args(argv[2:]) - - if len(argv) < 3 and command == "help": - option_parser.print_help() - return 0 - - if options.verbose > 1: - logging.basicConfig(level=logging.DEBUG) - - # Files used for configuration and state saving. - options.config_filename = os.environ.get("GCLIENT_FILE", ".gclient") - if options.gclientfile: - options.config_filename = options.gclientfile - options.entries_filename = options.config_filename + "_entries" - options.deps_file = "DEPS" - - options.platform = sys.platform - return DispatchCommand(command, options, args) + if argv[0] == '--help': + argv[0] = 'help' + options, args = parser.parse_args(argv[1:]) + return Command(argv[0])(parser, options, args) if "__main__" == __name__: try: - result = Main(sys.argv) + sys.exit(Main(sys.argv[1:])) except gclient_utils.Error, e: print >> sys.stderr, "Error: %s" % str(e) - result = 1 - sys.exit(result) + sys.exit(1) # vim: ts=2:sw=2:tw=80:et: diff --git a/tests/gclient_test.py b/tests/gclient_test.py index 688613a81..b9027d3cc 100755 --- a/tests/gclient_test.py +++ b/tests/gclient_test.py @@ -97,7 +97,8 @@ class TestCMDconfig(GclientTestCase): exception_msg = "required argument missing; see 'gclient help config'" self.mox.ReplayAll() - self.assertRaisesError(exception_msg, gclient.CMDconfig, self.Options(), ()) + self.assertRaisesError(exception_msg, gclient.CMDconfig, None, + self.Options(), ()) def testExistingClientFile(self): options = self.Options() @@ -106,7 +107,8 @@ class TestCMDconfig(GclientTestCase): gclient.os.path.exists(options.config_filename).AndReturn(True) self.mox.ReplayAll() - self.assertRaisesError(exception_msg, gclient.CMDconfig, options, (1,)) + self.assertRaisesError(exception_msg, gclient.CMDconfig, None, options, + (1,)) def testFromText(self): options = self.Options(spec='config_source_content') @@ -116,7 +118,7 @@ class TestCMDconfig(GclientTestCase): gclient.GClient.SaveConfig() self.mox.ReplayAll() - gclient.CMDconfig(options, (1,),) + gclient.CMDconfig(None, options, (1,),) def testCreateClientFile(self): options = self.Options() @@ -127,7 +129,7 @@ class TestCMDconfig(GclientTestCase): gclient.GClient.SaveConfig() self.mox.ReplayAll() - gclient.CMDconfig(options, + gclient.CMDconfig(None, options, ('http://svn/url/the_name', 'other', 'args', 'ignored')) @@ -138,7 +140,7 @@ class GenericCommandTestCase(GclientTestCase): gclient.GClient.RunOnDeps(command, self.args).AndReturn(return_value) self.mox.ReplayAll() - result = function(options, self.args) + result = function(None, options, self.args) self.assertEquals(result, return_value) def BadClient(self, function): @@ -148,7 +150,7 @@ class GenericCommandTestCase(GclientTestCase): self.mox.ReplayAll() self.assertRaisesError( "client not configured; see 'gclient config'", - function, options, self.args) + function, None, options, self.args) def Verbose(self, command, function): options = self.Options(verbose=True) @@ -159,7 +161,7 @@ class GenericCommandTestCase(GclientTestCase): gclient.GClient.RunOnDeps(command, self.args).AndReturn(0) self.mox.ReplayAll() - result = function(options, self.args) + result = function(None, options, self.args) self.assertEquals(result, 0) @@ -201,7 +203,7 @@ class TestCMDupdate(GenericCommandTestCase): gclient.GClient.RunOnDeps(command, self.args).AndReturn(return_value) self.mox.ReplayAll() - result = function(options, self.args) + result = function(None, options, self.args) self.assertEquals(result, return_value) def Verbose(self, command, function): @@ -214,7 +216,7 @@ class TestCMDupdate(GenericCommandTestCase): gclient.GClient.RunOnDeps(command, self.args).AndReturn(0) self.mox.ReplayAll() - result = function(options, self.args) + result = function(None, options, self.args) self.assertEquals(result, 0) def Options(self, verbose=False, *args, **kwargs):