diff --git a/deluge/tests/test_ui_entry.py b/deluge/tests/test_ui_entry.py index 8163ef157..920c496c9 100644 --- a/deluge/tests/test_ui_entry.py +++ b/deluge/tests/test_ui_entry.py @@ -1,16 +1,78 @@ # -*- coding: utf-8 -*- +from __future__ import print_function + +import argparse +import exceptions +import StringIO +import sys + import mock import pytest import deluge.component as component +import deluge.ui.console +import deluge.ui.web.server from deluge.ui import ui_entry +from deluge.ui.web.server import DelugeWeb from . import common from .basetest import BaseTestCase +sys_stdout = sys.stdout + + +class TestStdout(object): + + def __init__(self, fd): + self.out = StringIO.StringIO() + self.fd = fd + for a in ["encoding"]: + setattr(self, a, getattr(sys_stdout, a)) + + def write(self, *data, **kwargs): + print(data, file=self.out) + + def flush(self): + self.out.flush() + + +class DelugeEntryTestCase(BaseTestCase): + + def set_up(self): + common.set_tmp_config_dir() + return component.start() + + def tear_down(self): + return component.shutdown() + + def test_deluge_help(self): + self.patch(sys, "argv", ["./deluge", "-h"]) + config = deluge.configmanager.ConfigManager("ui.conf", ui_entry.DEFAULT_PREFS) + config.config["default_ui"] = "console" + config.save() + + fd = TestStdout(sys.stdout) + self.patch(argparse._sys, "stdout", fd) + + with mock.patch("deluge.ui.console.main.ConsoleUI"): + self.assertRaises(exceptions.SystemExit, ui_entry.start_ui) + self.assertTrue("usage: deluge" in fd.out.getvalue()) + self.assertTrue("UI Options:" in fd.out.getvalue()) + self.assertTrue("* console" in fd.out.getvalue()) + + def test_start_default(self): + self.patch(sys, "argv", ["./deluge"]) + config = deluge.configmanager.ConfigManager("ui.conf", ui_entry.DEFAULT_PREFS) + config.config["default_ui"] = "console" + config.save() + + with mock.patch("deluge.ui.console.main.ConsoleUI"): + # Just test that no exception is raised + ui_entry.start_ui() + @pytest.mark.gtkui -class UIEntryTestCase(BaseTestCase): +class GtkUIEntryTestCase(BaseTestCase): def set_up(self): common.set_tmp_config_dir() @@ -20,30 +82,108 @@ class UIEntryTestCase(BaseTestCase): return component.shutdown() def test_start_gtkui(self): - import deluge.ui.gtkui.gtkui - import sys - self.patch(sys, "argv", ['./deluge', "--ui", 'gtk']) + self.patch(sys, "argv", ["./deluge", "gtk"]) - with mock.patch.object(deluge.ui.gtkui.gtkui.GtkUI, 'start', autospec=True): + from deluge.ui.gtkui import gtkui + with mock.patch.object(gtkui.GtkUI, "start", autospec=True): ui_entry.start_ui() - def test_start_console(self): - import sys - self.patch(sys, "argv", ['./deluge', "--ui", 'console']) - with mock.patch('deluge.ui.console.main.ConsoleUI'): - ui_entry.start_ui() + +class WebUIEntryTestCase(BaseTestCase): + + def set_up(self): + common.set_tmp_config_dir() + return component.start() + + def tear_down(self): + return component.shutdown() def test_start_webserver(self): - import sys - from deluge.ui.web.server import DelugeWeb - - self.patch(sys, "argv", ['./deluge', "--ui", 'web', '--do-not-daemonize']) + self.patch(sys, "argv", ["./deluge", "web", "--do-not-daemonize"]) class DelugeWebMock(DelugeWeb): def __init__(self, *args, **kwargs): kwargs["daemon"] = False DelugeWeb.__init__(self, *args, **kwargs) - import deluge.ui.web.server - self.patch(deluge.ui.web.server, 'DelugeWeb', DelugeWebMock) + self.patch(deluge.ui.web.server, "DelugeWeb", DelugeWebMock) ui_entry.start_ui() + + +class ConsoleUIBaseTestCase(object): + + def __init__(self): + self.var = dict() + + def set_up(self): + common.set_tmp_config_dir() + return component.start() + + def tear_down(self): + return component.shutdown() + + def test_start_console(self): + self.patch(sys, "argv", self.var["sys_arg_cmd"]) + with mock.patch("deluge.ui.console.main.ConsoleUI"): + self.var["start_cmd"]() + + def test_console_help(self): + self.patch(sys, "argv", self.var["sys_arg_cmd"] + ["-h"]) + fd = TestStdout(sys.stdout) + self.patch(argparse._sys, "stdout", fd) + + with mock.patch("deluge.ui.console.main.ConsoleUI"): + self.assertRaises(exceptions.SystemExit, self.var["start_cmd"]) + std_output = fd.out.getvalue() + self.assertTrue(("usage: %s" % self.var["cmd_name"]) in std_output) # Check command name + self.assertTrue("Common Options:" in std_output) + self.assertTrue("Console Options:" in std_output) + self.assertTrue(r"Console commands:\n The following console commands are available:" in std_output) + self.assertTrue("The following console commands are available:" in std_output) + + def test_console_command_info(self): + self.patch(sys, "argv", self.var["sys_arg_cmd"] + ["info"]) + fd = TestStdout(sys.stdout) + self.patch(argparse._sys, "stdout", fd) + + with mock.patch("deluge.ui.console.main.ConsoleUI"): + self.var["start_cmd"]() + + def test_console_command_info_help(self): + self.patch(sys, "argv", self.var["sys_arg_cmd"] + ["info", "-h"]) + fd = TestStdout(sys.stdout) + self.patch(argparse._sys, "stdout", fd) + + with mock.patch("deluge.ui.console.main.ConsoleUI"): + self.assertRaises(exceptions.SystemExit, self.var["start_cmd"]) + std_output = fd.out.getvalue() + self.assertTrue("usage: info" in std_output) + self.assertTrue("Show information about the torrents" in std_output) + + def test_console_unrecognized_arguments(self): + self.patch(sys, "argv", ["./deluge", "--ui", "console"]) # --ui is not longer supported + fd = TestStdout(sys.stdout) + self.patch(argparse._sys, "stderr", fd) + with mock.patch("deluge.ui.console.main.ConsoleUI"): + self.assertRaises(exceptions.SystemExit, self.var["start_cmd"]) + self.assertTrue("unrecognized arguments: --ui" in fd.out.getvalue()) + + +class ConsoleUIEntryTestCase(BaseTestCase, ConsoleUIBaseTestCase): + + def __init__(self, testname): + BaseTestCase.__init__(self, testname) + ConsoleUIBaseTestCase.__init__(self) + self.var["cmd_name"] = "deluge console" + self.var["start_cmd"] = ui_entry.start_ui + self.var["sys_arg_cmd"] = ["./deluge", "console"] + + +class ConsoleUIScriptTestCase(BaseTestCase, ConsoleUIBaseTestCase): + + def __init__(self, testname): + BaseTestCase.__init__(self, testname) + ConsoleUIBaseTestCase.__init__(self) + self.var["cmd_name"] = "deluge-console" + self.var["start_cmd"] = deluge.ui.console.start + self.var["sys_arg_cmd"] = ["./deluge-console"] diff --git a/deluge/ui/baseargparser.py b/deluge/ui/baseargparser.py index f137bc983..d37157498 100644 --- a/deluge/ui/baseargparser.py +++ b/deluge/ui/baseargparser.py @@ -20,6 +20,69 @@ from deluge.configmanager import get_config_dir, set_config_dir from deluge.log import setup_logger +def find_subcommand(self, args=None): + """Find if a subcommand has been supplied. + + Args: + args (list, optional): The argument list handed to parse_args(). + + Returns: + int: Index of the subcommand or '-1' if none found. + + """ + subcommand_found = -1 + test_args = args if args else sys.argv[1:] + + for x in self._subparsers._actions: + if not isinstance(x, argparse._SubParsersAction): + continue + for sp_name in x._name_parser_map.keys(): + if sp_name in test_args: + subcommand_found = test_args.index(sp_name) + 1 + + return subcommand_found + + +def set_default_subparser(self, name, abort_opts=None): + """Sets the default argparse subparser. + + Args: + name (str): The name of the default subparser. + abort_opts (list): The arguments to test for in case no subcommand is found. + If any of the values are found, the default subparser will + not be inserted into sys.argv. + + Returns: + list: The arguments found in sys.argv if no subcommand found, else None + + """ + found_abort_opts = [] + abort_opts = [] if abort_opts is None else abort_opts + test_args = sys.argv[1:] + subparser_found = self.find_subcommand(args=test_args) + + for i, arg in enumerate(test_args): + if subparser_found == i: + break + if arg in abort_opts: + found_abort_opts.append(arg) + + if subparser_found == -1: + if found_abort_opts: + # Found one or more of arguments in abort_opts + return found_abort_opts + + # insert default in first position, this implies no + # global options without a sub_parsers specified + sys.argv.insert(1, name) + + return None + + +argparse.ArgumentParser.find_subcommand = find_subcommand +argparse.ArgumentParser.set_default_subparser = set_default_subparser + + def get_version(): version_str = "%s\n" % (common.get_version()) try: @@ -33,8 +96,7 @@ def get_version(): class DelugeTextHelpFormatter(argparse.RawDescriptionHelpFormatter): - """Help message formatter which retains formatting of all help text. - """ + """Help message formatter which retains formatting of all help text.""" def _split_lines(self, text, width): """ @@ -80,9 +142,9 @@ class HelpAction(argparse._HelpAction): def __call__(self, parser, namespace, values, option_string=None): if hasattr(parser, "subparser"): subparser = getattr(parser, "subparser") - # If -h on a subparser is given, the subparser will exit after help message - subparser.parse_args() - parser.print_help() + subparser.print_help() + else: + parser.print_help() parser.exit() @@ -91,13 +153,16 @@ class BaseArgParser(argparse.ArgumentParser): def __init__(self, *args, **kwargs): if "formatter_class" not in kwargs: kwargs["formatter_class"] = lambda prog: DelugeTextHelpFormatter(prog, max_help_position=33, width=90) - super(BaseArgParser, self).__init__(*args, add_help=False, **kwargs) + kwargs["add_help"] = kwargs.get("add_help", False) + common_help = kwargs.pop("common_help", True) + super(BaseArgParser, self).__init__(*args, **kwargs) self.common_setup = False self.process_arg_group = False self.group = self.add_argument_group(_("Common Options")) - self.group.add_argument("-h", "--help", action=HelpAction, - help=_("Print this help message")) + if common_help: + self.group.add_argument("-h", "--help", action=HelpAction, + help=_("Print this help message")) self.group.add_argument("-V", "--version", action="version", version="%(prog)s " + get_version(), help=_("Print version information")) self.group.add_argument("-v", action="version", version="%(prog)s " + get_version(), @@ -117,9 +182,22 @@ class BaseArgParser(argparse.ArgumentParser): help=_("Profile %(prog)s with cProfile. Outputs to stdout " "unless a filename is specified")) - def parse_args(self, *args): - options, remaining = super(BaseArgParser, self).parse_known_args(*args) + def parse_args(self, args=None): + """Parse UI arguments and handle common and process group options. + + Unknown arguments return an error and resulting usage text. + """ + options = super(BaseArgParser, self).parse_args(args=args) + return self._parse_args(options) + + def parse_known_ui_args(self, args=None): + """Parse UI arguments and handle common and process group options without error. + """ + options, remaining = super(BaseArgParser, self).parse_known_args(args=args) options.remaining = remaining + return self._parse_args(options) + + def _parse_args(self, options): if not self.common_setup: self.common_setup = True diff --git a/deluge/ui/console/commander.py b/deluge/ui/console/commander.py index e1c59825d..2d23687fc 100644 --- a/deluge/ui/console/commander.py +++ b/deluge/ui/console/commander.py @@ -32,10 +32,28 @@ class Commander(object): print(strip_colors(line)) def do_command(self, cmd_line): - """ - Processes a command. + """Run a console command - :param cmd: str, the command string + Args: + cmd_line (str): Console command + + Returns: + Deferred: A deferred that fires when command has been executed + + """ + options = self.parse_command(cmd_line) + if options: + return self.exec_command(options) + return defer.succeed(None) + + def parse_command(self, cmd_line): + """Parse a console command and process with argparse + + Args: + cmd_line (str): Console command + + Returns: + argparse.Namespace: The parsed command """ if not cmd_line: @@ -76,6 +94,7 @@ class Commander(object): try: options = parser.parse_args(args=args) + options.command = cmd except TypeError as ex: self.write("{!error!}Error parsing options: %s" % ex) import traceback @@ -86,14 +105,28 @@ class Commander(object): parser.print_help() return - if not getattr(parser, "_exit", False): - try: - ret = self._commands[cmd].handle(options) - except Exception as ex: - self.write("{!error!} %s" % ex) - log.exception(ex) - import traceback - self.write("%s" % traceback.format_exc()) - return defer.succeed(True) - else: - return ret + if getattr(parser, "_exit", False): + return + return options + + def exec_command(self, options, *args): + """ + Execute a console command. + + Args: + options (argparse.Namespace): The command to execute + + Returns: + Deferred: A deferred that fires when command has been executed + + """ + try: + ret = self._commands[options.command].handle(options) + except Exception as ex: + self.write("{!error!} %s" % ex) + log.exception(ex) + import traceback + self.write("%s" % traceback.format_exc()) + return defer.succeed(True) + else: + return ret diff --git a/deluge/ui/console/commands/info.py b/deluge/ui/console/commands/info.py index c03331469..dadd5134f 100644 --- a/deluge/ui/console/commands/info.py +++ b/deluge/ui/console/commands/info.py @@ -92,7 +92,7 @@ class Command(BaseCommand): epilog = """ You can give the first few characters of a torrent-id to identify the torrent. - Tab Completion (info *pattern*):\n + Tab Completion in interactive mode (info *pattern*):\n | First press of will output up to 15 matches; | hitting a second time, will print 15 more matches; | and a third press will print all remaining matches. diff --git a/deluge/ui/console/console.py b/deluge/ui/console/console.py index 4236ef271..f2a09b904 100644 --- a/deluge/ui/console/console.py +++ b/deluge/ui/console/console.py @@ -53,7 +53,7 @@ class Console(UI): cmd_description = """Console or command-line user interface""" def __init__(self, *args, **kwargs): - super(Console, self).__init__("console", *args, description="Test", **kwargs) + super(Console, self).__init__(*args, **kwargs) group = self.parser.add_argument_group(_("Console Options"), "These daemon connect options will be " "used for commands, or if console ui autoconnect is enabled.") @@ -65,20 +65,25 @@ class Console(UI): # To properly print help message for the console commands ( e.g. deluge-console info -h), # we add a subparser for each command which will trigger the help/usage when given from deluge.ui.console.main import ConsoleCommandParser # import here because (see top) - self.console_parser = ConsoleCommandParser(parents=[self.parser], add_help=False, + self.console_parser = ConsoleCommandParser(parents=[self.parser], add_help=False, prog=self.parser.prog, description="Starts the Deluge console interface", formatter_class=lambda prog: DelugeTextHelpFormatter(prog, max_help_position=33, width=90)) self.parser.subparser = self.console_parser - subparsers = self.console_parser.add_subparsers(title="Console commands", help="Description", dest="commands", + self.console_parser.base_parser = self.parser + subparsers = self.console_parser.add_subparsers(title="Console commands", help="Description", dest="command", description="The following console commands are available:", metavar="command") self.console_cmds = load_commands(os.path.join(UI_PATH, "commands")) for c in sorted(self.console_cmds): self.console_cmds[c].add_subparser(subparsers) - def start(self, args=None): - super(Console, self).start(args) + def start(self): + i = self.console_parser.find_subcommand(args=self.ui_args) + self.console_parser.subcommand = False + self.parser.subcommand = False if i == -1 else True + + super(Console, self).start(self.console_parser) from deluge.ui.console.main import ConsoleUI # import here because (see top) def run(options): diff --git a/deluge/ui/console/main.py b/deluge/ui/console/main.py index 99b80d41e..b10dca818 100644 --- a/deluge/ui/console/main.py +++ b/deluge/ui/console/main.py @@ -24,6 +24,7 @@ from deluge.error import DelugeError from deluge.ui.client import client from deluge.ui.console import colors from deluge.ui.console.colors import ConsoleColorFormatter +from deluge.ui.console.commander import Commander from deluge.ui.console.eventlog import EventLog from deluge.ui.console.statusbars import StatusBars from deluge.ui.coreconfig import CoreConfig @@ -49,6 +50,58 @@ class ConsoleCommandParser(argparse.ArgumentParser): self.epilog = epilog return help_str + def _split_args(self, args): + command_options = [] + for a in args: + if not a: + continue + if ";" in a: + cmd_lines = [arg.strip() for arg in a.split(";")] + elif " " in a: + cmd_lines = [a] + else: + continue + + for cmd_line in cmd_lines: + cmds = shlex.split(cmd_line) + cmd_options = super(ConsoleCommandParser, self).parse_args(args=cmds) + cmd_options.command = cmds[0] + command_options.append(cmd_options) + + return command_options + + def parse_args(self, args=None): + multi_command = self._split_args(args) + # If multiple commands were passed to console + if multi_command: + # With multiple commands, normal parsing will fail, so only parse + # known arguments using the base parser, and then set + # options.parsed_cmds to the already parsed commands + options, remaining = self.base_parser.parse_known_args(args=args) + options.parsed_cmds = multi_command + else: + subcommand = False + if hasattr(self.base_parser, "subcommand"): + subcommand = getattr(self.base_parser, "subcommand") + if not subcommand: + # We must use parse_known_args to handle case when no subcommand + # is provided, because argparse does not support parsing without + # a subcommand + options, remaining = self.base_parser.parse_known_args(args=args) + # If any options remain it means they do not exist. Reparse with + # parse_args to trigger help message + if remaining: + options = self.base_parser.parse_args(args=args) + options.parsed_cmds = [] + else: + options = super(ConsoleCommandParser, self).parse_args(args=args) + options.parsed_cmds = [options] + + if not hasattr(options, "remaining"): + options.remaining = [] + + return options + class OptionParser(ConsoleCommandParser): @@ -130,6 +183,7 @@ class BaseCommand(object): opts["usage"] = self.usage parser = OptionParser(**opts) parser.add_argument(self.name, metavar="") + parser.base_parser = parser self.add_arguments(parser) return parser @@ -167,8 +221,7 @@ class ConsoleUI(component.Component): # Set the interactive flag to indicate where we should print the output self.interactive = True self._commands = cmds - - if options.remaining: + if options.parsed_cmds: self.interactive = False if not cmds: print("Sorry, couldn't find any commands") @@ -194,14 +247,6 @@ Please use commands from the command line, e.g.:\n reactor.run() def exec_args(self, options): - args = options.remaining - commands = [] - if args: - cmd = " ".join([arg for arg in args]) - # Multiple commands split by ";" - commands += [arg.strip() for arg in cmd.split(";")] - - from deluge.ui.console.commander import Commander commander = Commander(self._commands) def on_connect(result): @@ -209,11 +254,14 @@ Please use commands from the command line, e.g.:\n def on_started(result): def do_command(result, cmd): return commander.do_command(cmd) + + def exec_command(result, cmd): + return commander.exec_command(cmd) d = defer.succeed(None) - for command in commands: - if command in ("quit", "exit"): + for command in options.parsed_cmds: + if command.command in ("quit", "exit"): break - d.addCallback(do_command, command) + d.addCallback(exec_command, command) d.addCallback(do_command, "quit") # We need to wait for the rpcs in start() to finish before processing @@ -230,17 +278,9 @@ Please use commands from the command line, e.g.:\n commander.do_command("quit") d = None - if not self.interactive: - if commands[0] is not None: - if commands[0].startswith("connect"): - d = commander.do_command(commands.pop(0)) - if d is None: - # Error parsing command - sys.exit(0) - elif "help" in commands: - commander.do_command("help") - sys.exit(0) - if not d: + if not self.interactive and options.parsed_cmds[0].command == "connect": + d = commander.do_command(options.parsed_cmds.pop(0)) + else: log.info("connect: host=%s, port=%s, username=%s, password=%s", options.daemon_addr, options.daemon_port, options.daemon_user, options.daemon_pass) d = client.connect(options.daemon_addr, options.daemon_port, options.daemon_user, options.daemon_pass) diff --git a/deluge/ui/gtkui/__init__.py b/deluge/ui/gtkui/__init__.py index 8e449b2c0..5836b7ecd 100644 --- a/deluge/ui/gtkui/__init__.py +++ b/deluge/ui/gtkui/__init__.py @@ -27,8 +27,8 @@ class Gtk(UI): help=_("Add one or more torrent files, torrent URLs or magnet URIs" " to a currently running Deluge GTK instance")) - def start(self, args=None): - super(Gtk, self).start(args) + def start(self): + super(Gtk, self).start() from deluge.ui.gtkui.gtkui import GtkUI import deluge.common diff --git a/deluge/ui/ui.py b/deluge/ui/ui.py index ecf120b61..591336201 100644 --- a/deluge/ui/ui.py +++ b/deluge/ui/ui.py @@ -25,16 +25,20 @@ except ImportError: class UI(object): + """ + Base class for UI implementations. - cmd_description = """Insert command description""" + """ + cmd_description = """Override with command description""" - def __init__(self, name="gtk", parser=None, **kwargs): + def __init__(self, name="gtk", **kwargs): self.__name = name + self.ui_args = kwargs.pop("ui_args", None) lang.setup_translations(setup_pygtk=(name == "gtk")) - self.__parser = parser if parser else BaseArgParser(**kwargs) + self.__parser = BaseArgParser(**kwargs) - def parse_args(self, args=None): - options = self.parser.parse_args(args) + def parse_args(self, parser, args=None): + options = parser.parse_args(args) if not hasattr(options, "remaining"): options.remaining = [] return options @@ -51,12 +55,11 @@ class UI(object): def options(self): return self.__options - def start(self, extra_args=None): + def start(self, parser=None): args = deluge.common.unicode_argv()[1:] - if extra_args: - args.extend(extra_args) - - self.__options = self.parse_args(args) + if parser is None: + parser = self.parser + self.__options = self.parse_args(parser, args) setproctitle("deluge-%s" % self.__name) diff --git a/deluge/ui/ui_entry.py b/deluge/ui/ui_entry.py index 334a13c32..b156516bd 100644 --- a/deluge/ui/ui_entry.py +++ b/deluge/ui/ui_entry.py @@ -13,7 +13,9 @@ """Main starting point for Deluge""" +import argparse import logging +import os import sys import pkg_resources @@ -32,27 +34,29 @@ def start_ui(): """Entry point for ui script""" lang.setup_translations() - # Setup the argument parser - parser = BaseArgParser() - group = parser.add_argument_group(_("UI Options")) - + # Get the registered UI entry points ui_entrypoints = dict([(entrypoint.name, entrypoint.load()) for entrypoint in pkg_resources.iter_entry_points("deluge.ui")]) + ui_titles = sorted(ui_entrypoints.keys()) - cmd_help = [_("The UI that you wish to launch. The UI choices are:")] - cmd_help.extend(["%s -- %s" % (k, getattr(v, "cmd_description", "")) for k, v in ui_entrypoints.iteritems()]) + def add_ui_options_group(_parser): + """Function to enable reuse of UI Options group""" + group = _parser.add_argument_group(_("UI Options")) + group.add_argument("-s", "--set-default-ui", dest="default_ui", choices=ui_titles, + help=_("Set the default UI to be run, when no UI is specified")) + return _parser - group.add_argument("-u", "--ui", action="store", - choices=ui_entrypoints.keys(), help="\n * ".join(cmd_help)) - group.add_argument("-a", "--args", action="store", - help=_('Arguments to pass to the UI. Multiple args must be quoted, e.g. -a "--option args"')) - group.add_argument("-s", "--set-default-ui", dest="default_ui", choices=ui_entrypoints.keys(), - help=_("Sets the default UI to be run when no UI is specified"), action="store") + # Setup parser with Common Options and add UI Options group. + parser = add_ui_options_group(BaseArgParser()) - options = parser.parse_args(deluge.common.unicode_argv()[1:]) + ambiguous_args = ["-h", "--help", "-v", "-V", "--version"] + # Parse arguments without help/version as this is handled later. + args = [a for a in sys.argv if a not in ambiguous_args] + options = parser.parse_known_ui_args(args) config = deluge.configmanager.ConfigManager("ui.conf", DEFAULT_PREFS) log = logging.getLogger(__name__) + log.info("Deluge ui %s", deluge.common.get_version()) if options.default_ui: config["default_ui"] = options.default_ui @@ -60,37 +64,54 @@ def start_ui(): log.info("The default UI has been changed to %s", options.default_ui) sys.exit(0) - log.info("Deluge ui %s", deluge.common.get_version()) - log.debug("options: %s", options) - - selected_ui = options.ui if options.ui else config["default_ui"] - - config.save() + default_ui = config["default_ui"] + config.save() # Save in case config didn't already exist. del config - # reconstruct arguments to hand off to child client - client_args = [] - if options.args: - import shlex - client_args.extend(shlex.split(options.args)) + # We have parsed and got the config dir needed to get the default UI + # Now create a parser for choosing the UI. We reuse the ui option group for + # parsing to succeed and the text displayed to user, but result is not used. + parser = add_ui_options_group(BaseArgParser(common_help=True)) + + # Create subparser for each registered UI. Empty title is used to remove unwanted positional text. + subparsers = parser.add_subparsers(dest="selected_ui", metavar="{%s} [UI args]" % ",".join(ui_titles), title=None, + help=_("Alternative UI to launch, with optional ui args \n (default UI: *)")) + for ui in ui_titles: + parser_ui = subparsers.add_parser(ui, common_help=False, + help=getattr(ui_entrypoints[ui], "cmd_description", "")) + parser_ui.add_argument("ui_args", nargs=argparse.REMAINDER) + # If the UI is set as default, indicate this in help by prefixing with a star. + subactions = subparsers._get_subactions() + prefix = "*" if ui == default_ui else " " + subactions[-1].dest = "%s %s" % (prefix, ui) + + # Insert a default UI subcommand unless one of the ambiguous_args are specified + parser.set_default_subparser(default_ui, abort_opts=ambiguous_args) + + # Only parse known arguments to leave the UIs to show a help message if parsing fails. + options, remaining = parser.parse_known_args() + selected_ui = options.selected_ui + ui_args = remaining + options.ui_args + + # Remove the UI argument before launching the UI. + sys.argv.remove(selected_ui) try: - ui = ui_entrypoints[selected_ui](parser=parser) + ui = ui_entrypoints[selected_ui](prog="%s %s" % (os.path.basename(sys.argv[0]), selected_ui), ui_args=ui_args) except KeyError as ex: - log.error("Unable to find the requested UI: '%s'. Please select a different UI with the '-u' option" - " or alternatively use the '-s' option to select a different default UI.", selected_ui) + log.error("Unable to find chosen UI: '%s'. Please choose a different UI " + "or use '--set-default-ui' to change default UI.", selected_ui) except ImportError as ex: import traceback error_type, error_value, tb = sys.exc_info() stack = traceback.extract_tb(tb) last_frame = stack[-1] if last_frame[0] == __file__: - log.error("Unable to find the requested UI: %s. Please select a different UI with the '-u' " - "option or alternatively use the '-s' option to select a different default UI.", selected_ui) + log.error("Unable to find chosen UI: '%s'. Please choose a different UI " + "or use '--set-default-ui' to change default UI.", selected_ui) else: log.exception(ex) - log.error("There was an error whilst launching the request UI: %s", selected_ui) - log.error("Look at the traceback above for more information.") + log.error("Encountered an error launching the request UI: %s", selected_ui) sys.exit(1) else: - ui.start(client_args) + ui.start() diff --git a/deluge/ui/web/web.py b/deluge/ui/web/web.py index 321bd0059..dd3aab5b7 100644 --- a/deluge/ui/web/web.py +++ b/deluge/ui/web/web.py @@ -42,8 +42,8 @@ class Web(UI): def server(self): return self.__server - def start(self, args=None): - super(Web, self).start(args) + def start(self): + super(Web, self).start() from deluge.ui.web import server self.__server = server.DelugeWeb(options=self.options)