Repository: mesos Updated Branches: refs/heads/master 5d77c5bcb -> 1cdd962bc
CLI: Renamed `lib/mesos` directory to `lib/cli`. This renames the `src/cli_new/lib/mesos` directory in order to avoid name clashes with the Python Scheduler/Executor module. Currently both modules are imported as `mesos`. This changes the CLI's name to `cli` instead. This will eventually allow the two modules to be merged into one. Review: https://reviews.apache.org/r/58248/ Project: http://git-wip-us.apache.org/repos/asf/mesos/repo Commit: http://git-wip-us.apache.org/repos/asf/mesos/commit/f130a567 Tree: http://git-wip-us.apache.org/repos/asf/mesos/tree/f130a567 Diff: http://git-wip-us.apache.org/repos/asf/mesos/diff/f130a567 Branch: refs/heads/master Commit: f130a567d89cd1147d8c75d15963f88d3bb964e6 Parents: 5d77c5b Author: Eric Chung <[email protected]> Authored: Thu Apr 6 17:42:35 2017 -0700 Committer: Joseph Wu <[email protected]> Committed: Thu Apr 6 17:56:47 2017 -0700 ---------------------------------------------------------------------- src/cli_new/bin/config.py | 2 +- src/cli_new/bin/main.py | 22 ++-- src/cli_new/lib/cli/__init__.py | 23 ++++ src/cli_new/lib/cli/docopt.py | 90 +++++++++++++ src/cli_new/lib/cli/exceptions.py | 25 ++++ src/cli_new/lib/cli/plugins/__init__.py | 22 ++++ src/cli_new/lib/cli/plugins/base.py | 176 +++++++++++++++++++++++++ src/cli_new/lib/cli/util.py | 151 +++++++++++++++++++++ src/cli_new/lib/mesos/__init__.py | 23 ---- src/cli_new/lib/mesos/docopt.py | 90 ------------- src/cli_new/lib/mesos/exceptions.py | 25 ---- src/cli_new/lib/mesos/plugins/__init__.py | 22 ---- src/cli_new/lib/mesos/plugins/base.py | 176 ------------------------- src/cli_new/lib/mesos/util.py | 151 --------------------- 14 files changed, 499 insertions(+), 499 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/mesos/blob/f130a567/src/cli_new/bin/config.py ---------------------------------------------------------------------- diff --git a/src/cli_new/bin/config.py b/src/cli_new/bin/config.py index 274f8c6..2f77dc4 100644 --- a/src/cli_new/bin/config.py +++ b/src/cli_new/bin/config.py @@ -24,7 +24,7 @@ import json import os import sys -from mesos.exceptions import CLIException +from cli.exceptions import CLIException # There is no version module included in this package. However, http://git-wip-us.apache.org/repos/asf/mesos/blob/f130a567/src/cli_new/bin/main.py ---------------------------------------------------------------------- diff --git a/src/cli_new/bin/main.py b/src/cli_new/bin/main.py index bbfb52c..efeca6e 100644 --- a/src/cli_new/bin/main.py +++ b/src/cli_new/bin/main.py @@ -21,10 +21,10 @@ This is the main executable of the mesos-cli. import sys import config -import mesos +import cli -from mesos.docopt import docopt -from mesos.exceptions import CLIException +from cli.docopt import docopt +from cli.exceptions import CLIException VERSION = "Mesos " + config.VERSION + " CLI" @@ -63,11 +63,11 @@ def autocomplete(cmds, plugins, current_word, argv): argv = argv[1:] comp_words = list(cmds.keys()) + ["help"] - comp_words = mesos.util.completions(comp_words, current_word, argv) + comp_words = cli.util.completions(comp_words, current_word, argv) if comp_words != None: return (option, comp_words) - plugin = mesos.util.get_module(plugins, argv[0]) + plugin = cli.util.get_module(plugins, argv[0]) plugin_class = getattr(plugin, plugin.PLUGIN_CLASS) return plugin_class(config).__autocomplete_base__(current_word, argv[1:]) @@ -79,18 +79,18 @@ def main(argv): """ # Initialize the various plugins. - plugins = mesos.util.import_modules(config.PLUGINS, "plugins") + plugins = cli.util.import_modules(config.PLUGINS, "plugins") cmds = { - mesos.util.get_module(plugins, plugin).PLUGIN_NAME: - mesos.util.get_module(plugins, plugin).SHORT_HELP + cli.util.get_module(plugins, plugin).PLUGIN_NAME: + cli.util.get_module(plugins, plugin).SHORT_HELP for plugin in plugins.keys() } # Parse all incoming arguments using docopt. command_strings = "" if cmds != {}: - command_strings = mesos.util.format_commands_help(cmds) + command_strings = cli.util.format_commands_help(cmds) usage = USAGE.format(commands=command_strings) arguments = docopt(usage, argv=argv, version=VERSION, options_first=True) @@ -123,7 +123,7 @@ def main(argv): # supplied command and its subcommands. elif cmd == "help": if len(argv) > 0 and argv[0] in cmds: - plugin = mesos.util.get_module(plugins, argv[0]) + plugin = cli.util.get_module(plugins, argv[0]) plugin_class = getattr(plugin, plugin.PLUGIN_CLASS) plugin_class(config).main(argv[1:] + ["--help"]) else: @@ -131,7 +131,7 @@ def main(argv): # Run the command through its plugin. elif cmd in cmds.keys(): - plugin = mesos.util.get_module(plugins, cmd) + plugin = cli.util.get_module(plugins, cmd) plugin_class = getattr(plugin, plugin.PLUGIN_CLASS) plugin_class(config).main(argv) http://git-wip-us.apache.org/repos/asf/mesos/blob/f130a567/src/cli_new/lib/cli/__init__.py ---------------------------------------------------------------------- diff --git a/src/cli_new/lib/cli/__init__.py b/src/cli_new/lib/cli/__init__.py new file mode 100644 index 0000000..f4fc3f1 --- /dev/null +++ b/src/cli_new/lib/cli/__init__.py @@ -0,0 +1,23 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Mesos Module +""" + +from . import exceptions +from . import plugins +from . import util http://git-wip-us.apache.org/repos/asf/mesos/blob/f130a567/src/cli_new/lib/cli/docopt.py ---------------------------------------------------------------------- diff --git a/src/cli_new/lib/cli/docopt.py b/src/cli_new/lib/cli/docopt.py new file mode 100644 index 0000000..86a4e9c --- /dev/null +++ b/src/cli_new/lib/cli/docopt.py @@ -0,0 +1,90 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Unfortunately, docopt doesn't support multi-word commands. This is +important for supporting things like: + +mesos cluster ps <args>... + +However, it looks like some plans are in place for supporting it in the +future: https://github.com/docopt/docopt/issues/41 + +The proposal is to add a "program" keyword argument to docopt to specify the +full set of words used to represent the command. Since this is not yet +supported officially, we include the hack below to make it work for us. We +essentially intercept the call to docopt and make it work with such a "program" +argument. + +To make it work, we inspect the value of "program" and search and replace all +instances of it in the usage string with a transformed version of it to make it +a single word (i.e. we replace all spaces with dashes: echo $program | s/ +/-/g). This essentially turns all multi-word commands in the usage string into +dash-separated single words (e.g., s/mesos cluster ps/mesos-cluster-ps/g). With +this in place, we then pass this usage string to the original docopt for +parsing. + +Unfortunately, doing things this way means that docopt (by default) will print +the usage string containing the dashes. To avoid this, we intercept all paths +where docopt does the printing itself, and transform the usage string back to +its original form. + +Hopefully we can remove this brutal hack at some point in the future +once docopt supports the "program" argument natively. +""" + +from __future__ import absolute_import +import os +import sys + +# pylint: disable=F0401 +from docopt import docopt as real_docopt, DocoptExit + + +def docopt(usage, **keywords): + """ A wrapper around the real docopt parser. """ + new_usage = usage + + if "program" in keywords: + program = keywords.pop("program") + new_usage = usage.replace(program, program.replace(" ", "-")) + + try: + stdout = sys.stdout + + with open(os.devnull, 'w') as nullfile: + sys.stdout = nullfile + arguments = real_docopt(new_usage, **keywords) + sys.stdout = stdout + + return arguments + + except DocoptExit: + sys.stdout = stdout + print >> sys.stderr, usage.strip() + sys.exit(1) + + except SystemExit: + sys.stdout = stdout + + if "argv" in keywords and any(h in ("-h", "--help") + for h in keywords["argv"]): + print usage.strip() + elif "version" in keywords and any(v in ("--version") + for v in keywords["argv"]): + print keywords["version"].strip() + + sys.exit() http://git-wip-us.apache.org/repos/asf/mesos/blob/f130a567/src/cli_new/lib/cli/exceptions.py ---------------------------------------------------------------------- diff --git a/src/cli_new/lib/cli/exceptions.py b/src/cli_new/lib/cli/exceptions.py new file mode 100644 index 0000000..c78f4a9 --- /dev/null +++ b/src/cli_new/lib/cli/exceptions.py @@ -0,0 +1,25 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +CLIException Class +""" + +class CLIException(Exception): + """ + Exceptions class to handle all CLI errors. + """ + pass http://git-wip-us.apache.org/repos/asf/mesos/blob/f130a567/src/cli_new/lib/cli/plugins/__init__.py ---------------------------------------------------------------------- diff --git a/src/cli_new/lib/cli/plugins/__init__.py b/src/cli_new/lib/cli/plugins/__init__.py new file mode 100644 index 0000000..e743772 --- /dev/null +++ b/src/cli_new/lib/cli/plugins/__init__.py @@ -0,0 +1,22 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Plugins Module +""" + +# pylint: disable=W0401 +from .base import * http://git-wip-us.apache.org/repos/asf/mesos/blob/f130a567/src/cli_new/lib/cli/plugins/base.py ---------------------------------------------------------------------- diff --git a/src/cli_new/lib/cli/plugins/base.py b/src/cli_new/lib/cli/plugins/base.py new file mode 100644 index 0000000..c10d70f --- /dev/null +++ b/src/cli_new/lib/cli/plugins/base.py @@ -0,0 +1,176 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Plugin's Base Class +""" + +import sys + +import cli + +from cli.docopt import docopt + + +PLUGIN_NAME = "base-plugin" +PLUGIN_CLASS = "PluginBase" + +VERSION = "Mesos Plugin Base 1.0" + +SHORT_HELP = "This is the base plugin from which all other plugins inherit." + +USAGE = \ +""" +{short_help} + +Usage: + mesos {plugin} (-h | --help) + mesos {plugin} --version + mesos {plugin} <command> (-h | --help) + mesos {plugin} <command> [<args>...] [options] + +Options: + -h --help Show this screen. + --version Show version info. + +Commands: +{commands} +""" + +SUBCOMMAND_USAGE = \ +"""{short_help} + +Usage: + mesos {plugin} {command} (-h | --help) + mesos {plugin} {command} --version + mesos {plugin} {command} {arguments} [options] + +Options: +{flags} + +Description: +{long_help} +""" + + +class PluginBase(object): + """ + Base class from which all CLI plugins should inherit. + """ + # pylint: disable=R0903 + COMMANDS = {} + + def __setup__(self, command, argv): + pass + + def __module_reference__(self): + return sys.modules[self.__module__] + + def __init__(self, config): + # pylint: disable=C0103 + self.PLUGIN_NAME = PLUGIN_NAME + self.PLUGIN_CLASS = PLUGIN_CLASS + self.VERSION = VERSION + self.SHORT_HELP = SHORT_HELP + self.USAGE = USAGE + + module = self.__module_reference__() + if hasattr(module, "PLUGIN_NAME"): + self.PLUGIN_NAME = getattr(module, "PLUGIN_NAME") + if hasattr(module, "PLUGIN_CLASS"): + self.PLUGIN_CLASS = getattr(module, "PLUGIN_CLASS") + if hasattr(module, "VERSION"): + self.VERSION = getattr(module, "VERSION") + if hasattr(module, "SHORT_HELP"): + self.SHORT_HELP = getattr(module, "SHORT_HELP") + if hasattr(module, "USAGE"): + self.USAGE = getattr(module, "USAGE") + + self.config = config + + def __autocomplete__(self, command, current_word, argv): + # pylint: disable=W0612,W0613,R0201 + return ("default", []) + + def __autocomplete_base__(self, current_word, argv): + option = "default" + + # <command> + comp_words = list(self.COMMANDS.keys()) + comp_words = cli.util.completions(comp_words, current_word, argv) + if comp_words != None: + return (option, comp_words) + + # <args>... + # pylint: disable=R0204 + comp_words = self.__autocomplete__(argv[0], current_word, argv[1:]) + + # In general, we expect a tuple to be returned from __autocomplete__, + # with the first element being a valid autocomplete option, and the + # second being a list of completion words. However, in the common + # case we usually use the default option, so it's OK for a plugin to + # just return a list. We will add the "default" option for them. + if isinstance(comp_words, tuple): + option, comp_words = comp_words + + return (option, comp_words) + + def main(self, argv): + """ + Main method takes argument from top level mesos and parses them + to call the appropriate method. + """ + command_strings = cli.util.format_commands_help(self.COMMANDS) + + usage = self.USAGE.format( + plugin=self.PLUGIN_NAME, + short_help=self.SHORT_HELP, + commands=command_strings) + + arguments = docopt( + usage, + argv=argv, + version=self.VERSION, + program="mesos " + self.PLUGIN_NAME, + options_first=True) + + cmd = arguments["<command>"] + argv = arguments["<args>"] + + if cmd in self.COMMANDS.keys(): + if "external" not in self.COMMANDS[cmd]: + argument_format, short_help, long_help, flag_format = \ + cli.util.format_subcommands_help(self.COMMANDS[cmd]) + + usage = SUBCOMMAND_USAGE.format( + plugin=self.PLUGIN_NAME, + command=cmd, + arguments=argument_format, + flags=flag_format, + short_help=short_help, + long_help=long_help) + + arguments = docopt( + usage, + argv=argv, + program="mesos " + self.PLUGIN_NAME + " " + cmd, + version=self.VERSION, + options_first=True) + + self.__setup__(cmd, argv) + getattr(self, cmd.replace("-", "_"))(arguments) + else: + self.main(["--help"]) http://git-wip-us.apache.org/repos/asf/mesos/blob/f130a567/src/cli_new/lib/cli/util.py ---------------------------------------------------------------------- diff --git a/src/cli_new/lib/cli/util.py b/src/cli_new/lib/cli/util.py new file mode 100644 index 0000000..ace07fb --- /dev/null +++ b/src/cli_new/lib/cli/util.py @@ -0,0 +1,151 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +A collection of helper functions used by the CLI and its Plugins. +""" + +import imp +import importlib +import os +import textwrap + +from cli.exceptions import CLIException + + +def import_modules(package_paths, module_type): + """ + Looks for python packages under `package_paths` and imports + them as modules. Returns a dictionary of the basename of the + `package_paths` to the imported modules. + """ + modules = {} + for package_path in package_paths: + # We put the imported module into the namespace of + # "mesos.<module_type>.<>" to keep it from cluttering up + # the import namespace elsewhere. + package_name = os.path.basename(package_path) + package_dir = os.path.dirname(package_path) + module_name = "cli." + module_type + "." + package_name + try: + module = importlib.import_module(module_name) + except Exception: + obj, filename, data = imp.find_module(package_name, [package_dir]) + module = imp.load_module(module_name, obj, filename, data) + modules[package_name] = module + + return modules + + +def get_module(modules, import_path): + """ + Given a modules dictionary returned by `import_modules()`, + return a reference to the module at `import_path` relative + to the base module. For example, get_module(modules, "example.stuff") + will return a reference to the "stuff" module inside the + imported "example" plugin. + """ + import_path = import_path.split('.') + try: + module = modules[import_path[0]] + if len(import_path) > 1: + module = getattr(module, ".".join(import_path[1:])) + except Exception as exception: + raise CLIException("Unable to get module: {error}" + .format(error=str(exception))) + + return module + + +def completions(comp_words, current_word, argv): + """ + Helps autocomplete by returning the appropriate + completion words under three conditions. + + 1) Returns `comp_words` if the completion word is + potentially in that list. + 2) Returns an empty list if there is no possible + completion. + 3) Returns `None` if the autocomplete is already done. + """ + comp_words += ["-h", "--help", "--version"] + + if len(argv) == 0: + return comp_words + + if len(argv) == 1: + if argv[0] not in comp_words and current_word: + return comp_words + + if argv[0] in comp_words and current_word: + return comp_words + + if argv[0] not in comp_words and not current_word: + return [] + + if argv[0] in comp_words and not current_word: + return None + + if len(argv) > 1 and argv[0] not in comp_words: + return [] + + if len(argv) > 1 and argv[0] in comp_words: + return None + + raise CLIException("Unreachable") + + +def format_commands_help(cmds): + """ + Helps format plugin commands for display. + """ + longest_cmd_name = max(cmds.keys(), key=len) + + help_string = "" + for cmd in sorted(cmds.keys()): + # For the top-level entry point, `cmds` is a single-level + # dictionary with `short_help` as the values. For plugins, + # `cmds` is a two-level dictionary, where `short_help` is a + # field in each sub-dictionary. + short_help = cmds[cmd] + if isinstance(short_help, dict): + short_help = short_help["short_help"] + + num_spaces = len(longest_cmd_name) - len(cmd) + 2 + help_string += " %s%s%s\n" % (cmd, " " * num_spaces, short_help) + + return help_string + + +def format_subcommands_help(cmd): + """ + Helps format plugin subcommands for display. + """ + arguments = " ".join(cmd["arguments"]) + short_help = cmd["short_help"] + long_help = textwrap.dedent(cmd["long_help"].rstrip()) + long_help = " " + "\n ".join(long_help.split('\n')) + flags = cmd["flags"] + flags["-h --help"] = "Show this screen." + flag_string = "" + + if len(flags.keys()) != 0: + longest_flag_name = max(flags.keys(), key=len) + for flag in sorted(flags.keys()): + num_spaces = len(longest_flag_name) - len(flag) + 2 + flag_string += " %s%s%s\n" % (flag, " " * num_spaces, flags[flag]) + + return (arguments, short_help, long_help, flag_string) http://git-wip-us.apache.org/repos/asf/mesos/blob/f130a567/src/cli_new/lib/mesos/__init__.py ---------------------------------------------------------------------- diff --git a/src/cli_new/lib/mesos/__init__.py b/src/cli_new/lib/mesos/__init__.py deleted file mode 100644 index f4fc3f1..0000000 --- a/src/cli_new/lib/mesos/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Mesos Module -""" - -from . import exceptions -from . import plugins -from . import util http://git-wip-us.apache.org/repos/asf/mesos/blob/f130a567/src/cli_new/lib/mesos/docopt.py ---------------------------------------------------------------------- diff --git a/src/cli_new/lib/mesos/docopt.py b/src/cli_new/lib/mesos/docopt.py deleted file mode 100644 index 86a4e9c..0000000 --- a/src/cli_new/lib/mesos/docopt.py +++ /dev/null @@ -1,90 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Unfortunately, docopt doesn't support multi-word commands. This is -important for supporting things like: - -mesos cluster ps <args>... - -However, it looks like some plans are in place for supporting it in the -future: https://github.com/docopt/docopt/issues/41 - -The proposal is to add a "program" keyword argument to docopt to specify the -full set of words used to represent the command. Since this is not yet -supported officially, we include the hack below to make it work for us. We -essentially intercept the call to docopt and make it work with such a "program" -argument. - -To make it work, we inspect the value of "program" and search and replace all -instances of it in the usage string with a transformed version of it to make it -a single word (i.e. we replace all spaces with dashes: echo $program | s/ -/-/g). This essentially turns all multi-word commands in the usage string into -dash-separated single words (e.g., s/mesos cluster ps/mesos-cluster-ps/g). With -this in place, we then pass this usage string to the original docopt for -parsing. - -Unfortunately, doing things this way means that docopt (by default) will print -the usage string containing the dashes. To avoid this, we intercept all paths -where docopt does the printing itself, and transform the usage string back to -its original form. - -Hopefully we can remove this brutal hack at some point in the future -once docopt supports the "program" argument natively. -""" - -from __future__ import absolute_import -import os -import sys - -# pylint: disable=F0401 -from docopt import docopt as real_docopt, DocoptExit - - -def docopt(usage, **keywords): - """ A wrapper around the real docopt parser. """ - new_usage = usage - - if "program" in keywords: - program = keywords.pop("program") - new_usage = usage.replace(program, program.replace(" ", "-")) - - try: - stdout = sys.stdout - - with open(os.devnull, 'w') as nullfile: - sys.stdout = nullfile - arguments = real_docopt(new_usage, **keywords) - sys.stdout = stdout - - return arguments - - except DocoptExit: - sys.stdout = stdout - print >> sys.stderr, usage.strip() - sys.exit(1) - - except SystemExit: - sys.stdout = stdout - - if "argv" in keywords and any(h in ("-h", "--help") - for h in keywords["argv"]): - print usage.strip() - elif "version" in keywords and any(v in ("--version") - for v in keywords["argv"]): - print keywords["version"].strip() - - sys.exit() http://git-wip-us.apache.org/repos/asf/mesos/blob/f130a567/src/cli_new/lib/mesos/exceptions.py ---------------------------------------------------------------------- diff --git a/src/cli_new/lib/mesos/exceptions.py b/src/cli_new/lib/mesos/exceptions.py deleted file mode 100644 index c78f4a9..0000000 --- a/src/cli_new/lib/mesos/exceptions.py +++ /dev/null @@ -1,25 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -CLIException Class -""" - -class CLIException(Exception): - """ - Exceptions class to handle all CLI errors. - """ - pass http://git-wip-us.apache.org/repos/asf/mesos/blob/f130a567/src/cli_new/lib/mesos/plugins/__init__.py ---------------------------------------------------------------------- diff --git a/src/cli_new/lib/mesos/plugins/__init__.py b/src/cli_new/lib/mesos/plugins/__init__.py deleted file mode 100644 index e743772..0000000 --- a/src/cli_new/lib/mesos/plugins/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Plugins Module -""" - -# pylint: disable=W0401 -from .base import * http://git-wip-us.apache.org/repos/asf/mesos/blob/f130a567/src/cli_new/lib/mesos/plugins/base.py ---------------------------------------------------------------------- diff --git a/src/cli_new/lib/mesos/plugins/base.py b/src/cli_new/lib/mesos/plugins/base.py deleted file mode 100644 index 61b1542..0000000 --- a/src/cli_new/lib/mesos/plugins/base.py +++ /dev/null @@ -1,176 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Plugin's Base Class -""" - -import sys - -import mesos - -from mesos.docopt import docopt - - -PLUGIN_NAME = "base-plugin" -PLUGIN_CLASS = "PluginBase" - -VERSION = "Mesos Plugin Base 1.0" - -SHORT_HELP = "This is the base plugin from which all other plugins inherit." - -USAGE = \ -""" -{short_help} - -Usage: - mesos {plugin} (-h | --help) - mesos {plugin} --version - mesos {plugin} <command> (-h | --help) - mesos {plugin} <command> [<args>...] [options] - -Options: - -h --help Show this screen. - --version Show version info. - -Commands: -{commands} -""" - -SUBCOMMAND_USAGE = \ -"""{short_help} - -Usage: - mesos {plugin} {command} (-h | --help) - mesos {plugin} {command} --version - mesos {plugin} {command} {arguments} [options] - -Options: -{flags} - -Description: -{long_help} -""" - - -class PluginBase(object): - """ - Base class from which all CLI plugins should inherit. - """ - # pylint: disable=R0903 - COMMANDS = {} - - def __setup__(self, command, argv): - pass - - def __module_reference__(self): - return sys.modules[self.__module__] - - def __init__(self, config): - # pylint: disable=C0103 - self.PLUGIN_NAME = PLUGIN_NAME - self.PLUGIN_CLASS = PLUGIN_CLASS - self.VERSION = VERSION - self.SHORT_HELP = SHORT_HELP - self.USAGE = USAGE - - module = self.__module_reference__() - if hasattr(module, "PLUGIN_NAME"): - self.PLUGIN_NAME = getattr(module, "PLUGIN_NAME") - if hasattr(module, "PLUGIN_CLASS"): - self.PLUGIN_CLASS = getattr(module, "PLUGIN_CLASS") - if hasattr(module, "VERSION"): - self.VERSION = getattr(module, "VERSION") - if hasattr(module, "SHORT_HELP"): - self.SHORT_HELP = getattr(module, "SHORT_HELP") - if hasattr(module, "USAGE"): - self.USAGE = getattr(module, "USAGE") - - self.config = config - - def __autocomplete__(self, command, current_word, argv): - # pylint: disable=W0612,W0613,R0201 - return ("default", []) - - def __autocomplete_base__(self, current_word, argv): - option = "default" - - # <command> - comp_words = list(self.COMMANDS.keys()) - comp_words = mesos.util.completions(comp_words, current_word, argv) - if comp_words != None: - return (option, comp_words) - - # <args>... - # pylint: disable=R0204 - comp_words = self.__autocomplete__(argv[0], current_word, argv[1:]) - - # In general, we expect a tuple to be returned from __autocomplete__, - # with the first element being a valid autocomplete option, and the - # second being a list of completion words. However, in the common - # case we usually use the default option, so it's OK for a plugin to - # just return a list. We will add the "default" option for them. - if isinstance(comp_words, tuple): - option, comp_words = comp_words - - return (option, comp_words) - - def main(self, argv): - """ - Main method takes argument from top level mesos and parses them - to call the appropriate method. - """ - command_strings = mesos.util.format_commands_help(self.COMMANDS) - - usage = self.USAGE.format( - plugin=self.PLUGIN_NAME, - short_help=self.SHORT_HELP, - commands=command_strings) - - arguments = docopt( - usage, - argv=argv, - version=self.VERSION, - program="mesos " + self.PLUGIN_NAME, - options_first=True) - - cmd = arguments["<command>"] - argv = arguments["<args>"] - - if cmd in self.COMMANDS.keys(): - if "external" not in self.COMMANDS[cmd]: - argument_format, short_help, long_help, flag_format = \ - mesos.util.format_subcommands_help(self.COMMANDS[cmd]) - - usage = SUBCOMMAND_USAGE.format( - plugin=self.PLUGIN_NAME, - command=cmd, - arguments=argument_format, - flags=flag_format, - short_help=short_help, - long_help=long_help) - - arguments = docopt( - usage, - argv=argv, - program="mesos " + self.PLUGIN_NAME + " " + cmd, - version=self.VERSION, - options_first=True) - - self.__setup__(cmd, argv) - getattr(self, cmd.replace("-", "_"))(arguments) - else: - self.main(["--help"]) http://git-wip-us.apache.org/repos/asf/mesos/blob/f130a567/src/cli_new/lib/mesos/util.py ---------------------------------------------------------------------- diff --git a/src/cli_new/lib/mesos/util.py b/src/cli_new/lib/mesos/util.py deleted file mode 100644 index 87d2a65..0000000 --- a/src/cli_new/lib/mesos/util.py +++ /dev/null @@ -1,151 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -A collection of helper functions used by the CLI and its Plugins. -""" - -import imp -import importlib -import os -import textwrap - -from mesos.exceptions import CLIException - - -def import_modules(package_paths, module_type): - """ - Looks for python packages under `package_paths` and imports - them as modules. Returns a dictionary of the basename of the - `package_paths` to the imported modules. - """ - modules = {} - for package_path in package_paths: - # We put the imported module into the namespace of - # "mesos.<module_type>.<>" to keep it from cluttering up - # the import namespace elsewhere. - package_name = os.path.basename(package_path) - package_dir = os.path.dirname(package_path) - module_name = "mesos." + module_type + "." + package_name - try: - module = importlib.import_module(module_name) - except Exception: - obj, filename, data = imp.find_module(package_name, [package_dir]) - module = imp.load_module(module_name, obj, filename, data) - modules[package_name] = module - - return modules - - -def get_module(modules, import_path): - """ - Given a modules dictionary returned by `import_modules()`, - return a reference to the module at `import_path` relative - to the base module. For example, get_module(modules, "example.stuff") - will return a reference to the "stuff" module inside the - imported "example" plugin. - """ - import_path = import_path.split('.') - try: - module = modules[import_path[0]] - if len(import_path) > 1: - module = getattr(module, ".".join(import_path[1:])) - except Exception as exception: - raise CLIException("Unable to get module: {error}" - .format(error=str(exception))) - - return module - - -def completions(comp_words, current_word, argv): - """ - Helps autocomplete by returning the appropriate - completion words under three conditions. - - 1) Returns `comp_words` if the completion word is - potentially in that list. - 2) Returns an empty list if there is no possible - completion. - 3) Returns `None` if the autocomplete is already done. - """ - comp_words += ["-h", "--help", "--version"] - - if len(argv) == 0: - return comp_words - - if len(argv) == 1: - if argv[0] not in comp_words and current_word: - return comp_words - - if argv[0] in comp_words and current_word: - return comp_words - - if argv[0] not in comp_words and not current_word: - return [] - - if argv[0] in comp_words and not current_word: - return None - - if len(argv) > 1 and argv[0] not in comp_words: - return [] - - if len(argv) > 1 and argv[0] in comp_words: - return None - - raise CLIException("Unreachable") - - -def format_commands_help(cmds): - """ - Helps format plugin commands for display. - """ - longest_cmd_name = max(cmds.keys(), key=len) - - help_string = "" - for cmd in sorted(cmds.keys()): - # For the top-level entry point, `cmds` is a single-level - # dictionary with `short_help` as the values. For plugins, - # `cmds` is a two-level dictionary, where `short_help` is a - # field in each sub-dictionary. - short_help = cmds[cmd] - if isinstance(short_help, dict): - short_help = short_help["short_help"] - - num_spaces = len(longest_cmd_name) - len(cmd) + 2 - help_string += " %s%s%s\n" % (cmd, " " * num_spaces, short_help) - - return help_string - - -def format_subcommands_help(cmd): - """ - Helps format plugin subcommands for display. - """ - arguments = " ".join(cmd["arguments"]) - short_help = cmd["short_help"] - long_help = textwrap.dedent(cmd["long_help"].rstrip()) - long_help = " " + "\n ".join(long_help.split('\n')) - flags = cmd["flags"] - flags["-h --help"] = "Show this screen." - flag_string = "" - - if len(flags.keys()) != 0: - longest_flag_name = max(flags.keys(), key=len) - for flag in sorted(flags.keys()): - num_spaces = len(longest_flag_name) - len(flag) + 2 - flag_string += " %s%s%s\n" % (flag, " " * num_spaces, flags[flag]) - - return (arguments, short_help, long_help, flag_string)
