[UI] [#1973] Improve passing extra args to UIs
Current solution for passing arguments to UI when invoking deluge entry script is to select an UI with the --ui option and supply quoted arguments with the --args option. This patch cleans this up by removing both options and change to using subparsers for valid UIs. All command line options are now parsed directly by the child UI which is chosen by a positional argument, i.e. the UI name. The help text now also shows the current default UI.
This commit is contained in:
parent
d6fec88932
commit
d689ad72e8
|
@ -1,16 +1,78 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import exceptions
|
||||||
|
import StringIO
|
||||||
|
import sys
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import deluge.component as component
|
import deluge.component as component
|
||||||
|
import deluge.ui.console
|
||||||
|
import deluge.ui.web.server
|
||||||
from deluge.ui import ui_entry
|
from deluge.ui import ui_entry
|
||||||
|
from deluge.ui.web.server import DelugeWeb
|
||||||
|
|
||||||
from . import common
|
from . import common
|
||||||
from .basetest import BaseTestCase
|
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
|
@pytest.mark.gtkui
|
||||||
class UIEntryTestCase(BaseTestCase):
|
class GtkUIEntryTestCase(BaseTestCase):
|
||||||
|
|
||||||
def set_up(self):
|
def set_up(self):
|
||||||
common.set_tmp_config_dir()
|
common.set_tmp_config_dir()
|
||||||
|
@ -20,30 +82,108 @@ class UIEntryTestCase(BaseTestCase):
|
||||||
return component.shutdown()
|
return component.shutdown()
|
||||||
|
|
||||||
def test_start_gtkui(self):
|
def test_start_gtkui(self):
|
||||||
import deluge.ui.gtkui.gtkui
|
self.patch(sys, "argv", ["./deluge", "gtk"])
|
||||||
import sys
|
|
||||||
self.patch(sys, "argv", ['./deluge', "--ui", '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()
|
ui_entry.start_ui()
|
||||||
|
|
||||||
def test_start_console(self):
|
|
||||||
import sys
|
class WebUIEntryTestCase(BaseTestCase):
|
||||||
self.patch(sys, "argv", ['./deluge', "--ui", 'console'])
|
|
||||||
with mock.patch('deluge.ui.console.main.ConsoleUI'):
|
def set_up(self):
|
||||||
ui_entry.start_ui()
|
common.set_tmp_config_dir()
|
||||||
|
return component.start()
|
||||||
|
|
||||||
|
def tear_down(self):
|
||||||
|
return component.shutdown()
|
||||||
|
|
||||||
def test_start_webserver(self):
|
def test_start_webserver(self):
|
||||||
import sys
|
self.patch(sys, "argv", ["./deluge", "web", "--do-not-daemonize"])
|
||||||
from deluge.ui.web.server import DelugeWeb
|
|
||||||
|
|
||||||
self.patch(sys, "argv", ['./deluge', "--ui", 'web', '--do-not-daemonize'])
|
|
||||||
|
|
||||||
class DelugeWebMock(DelugeWeb):
|
class DelugeWebMock(DelugeWeb):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
kwargs["daemon"] = False
|
kwargs["daemon"] = False
|
||||||
DelugeWeb.__init__(self, *args, **kwargs)
|
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()
|
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"]
|
||||||
|
|
|
@ -20,6 +20,69 @@ from deluge.configmanager import get_config_dir, set_config_dir
|
||||||
from deluge.log import setup_logger
|
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():
|
def get_version():
|
||||||
version_str = "%s\n" % (common.get_version())
|
version_str = "%s\n" % (common.get_version())
|
||||||
try:
|
try:
|
||||||
|
@ -33,8 +96,7 @@ def get_version():
|
||||||
|
|
||||||
|
|
||||||
class DelugeTextHelpFormatter(argparse.RawDescriptionHelpFormatter):
|
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):
|
def _split_lines(self, text, width):
|
||||||
"""
|
"""
|
||||||
|
@ -80,8 +142,8 @@ class HelpAction(argparse._HelpAction):
|
||||||
def __call__(self, parser, namespace, values, option_string=None):
|
def __call__(self, parser, namespace, values, option_string=None):
|
||||||
if hasattr(parser, "subparser"):
|
if hasattr(parser, "subparser"):
|
||||||
subparser = getattr(parser, "subparser")
|
subparser = getattr(parser, "subparser")
|
||||||
# If -h on a subparser is given, the subparser will exit after help message
|
subparser.print_help()
|
||||||
subparser.parse_args()
|
else:
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
parser.exit()
|
parser.exit()
|
||||||
|
|
||||||
|
@ -91,11 +153,14 @@ class BaseArgParser(argparse.ArgumentParser):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
if "formatter_class" not in kwargs:
|
if "formatter_class" not in kwargs:
|
||||||
kwargs["formatter_class"] = lambda prog: DelugeTextHelpFormatter(prog, max_help_position=33, width=90)
|
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.common_setup = False
|
||||||
self.process_arg_group = False
|
self.process_arg_group = False
|
||||||
self.group = self.add_argument_group(_("Common Options"))
|
self.group = self.add_argument_group(_("Common Options"))
|
||||||
|
if common_help:
|
||||||
self.group.add_argument("-h", "--help", action=HelpAction,
|
self.group.add_argument("-h", "--help", action=HelpAction,
|
||||||
help=_("Print this help message"))
|
help=_("Print this help message"))
|
||||||
self.group.add_argument("-V", "--version", action="version", version="%(prog)s " + get_version(),
|
self.group.add_argument("-V", "--version", action="version", version="%(prog)s " + get_version(),
|
||||||
|
@ -117,9 +182,22 @@ class BaseArgParser(argparse.ArgumentParser):
|
||||||
help=_("Profile %(prog)s with cProfile. Outputs to stdout "
|
help=_("Profile %(prog)s with cProfile. Outputs to stdout "
|
||||||
"unless a filename is specified"))
|
"unless a filename is specified"))
|
||||||
|
|
||||||
def parse_args(self, *args):
|
def parse_args(self, args=None):
|
||||||
options, remaining = super(BaseArgParser, self).parse_known_args(*args)
|
"""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
|
options.remaining = remaining
|
||||||
|
return self._parse_args(options)
|
||||||
|
|
||||||
|
def _parse_args(self, options):
|
||||||
|
|
||||||
if not self.common_setup:
|
if not self.common_setup:
|
||||||
self.common_setup = True
|
self.common_setup = True
|
||||||
|
|
|
@ -32,10 +32,28 @@ class Commander(object):
|
||||||
print(strip_colors(line))
|
print(strip_colors(line))
|
||||||
|
|
||||||
def do_command(self, cmd_line):
|
def do_command(self, cmd_line):
|
||||||
"""
|
"""Run a console command
|
||||||
Processes a 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:
|
if not cmd_line:
|
||||||
|
@ -76,6 +94,7 @@ class Commander(object):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
options = parser.parse_args(args=args)
|
options = parser.parse_args(args=args)
|
||||||
|
options.command = cmd
|
||||||
except TypeError as ex:
|
except TypeError as ex:
|
||||||
self.write("{!error!}Error parsing options: %s" % ex)
|
self.write("{!error!}Error parsing options: %s" % ex)
|
||||||
import traceback
|
import traceback
|
||||||
|
@ -86,9 +105,23 @@ class Commander(object):
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
return
|
return
|
||||||
|
|
||||||
if not getattr(parser, "_exit", False):
|
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:
|
try:
|
||||||
ret = self._commands[cmd].handle(options)
|
ret = self._commands[options.command].handle(options)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
self.write("{!error!} %s" % ex)
|
self.write("{!error!} %s" % ex)
|
||||||
log.exception(ex)
|
log.exception(ex)
|
||||||
|
|
|
@ -92,7 +92,7 @@ class Command(BaseCommand):
|
||||||
epilog = """
|
epilog = """
|
||||||
You can give the first few characters of a torrent-id to identify the torrent.
|
You can give the first few characters of a torrent-id to identify the torrent.
|
||||||
|
|
||||||
Tab Completion (info *pattern*<tab>):\n
|
Tab Completion in interactive mode (info *pattern*<tab>):\n
|
||||||
| First press of <tab> will output up to 15 matches;
|
| First press of <tab> will output up to 15 matches;
|
||||||
| hitting <tab> a second time, will print 15 more matches;
|
| hitting <tab> a second time, will print 15 more matches;
|
||||||
| and a third press will print all remaining matches.
|
| and a third press will print all remaining matches.
|
||||||
|
|
|
@ -53,7 +53,7 @@ class Console(UI):
|
||||||
cmd_description = """Console or command-line user interface"""
|
cmd_description = """Console or command-line user interface"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
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 "
|
group = self.parser.add_argument_group(_("Console Options"), "These daemon connect options will be "
|
||||||
"used for commands, or if console ui autoconnect is enabled.")
|
"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),
|
# 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
|
# 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)
|
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",
|
description="Starts the Deluge console interface",
|
||||||
formatter_class=lambda prog:
|
formatter_class=lambda prog:
|
||||||
DelugeTextHelpFormatter(prog, max_help_position=33, width=90))
|
DelugeTextHelpFormatter(prog, max_help_position=33, width=90))
|
||||||
self.parser.subparser = self.console_parser
|
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:",
|
description="The following console commands are available:",
|
||||||
metavar="command")
|
metavar="command")
|
||||||
self.console_cmds = load_commands(os.path.join(UI_PATH, "commands"))
|
self.console_cmds = load_commands(os.path.join(UI_PATH, "commands"))
|
||||||
for c in sorted(self.console_cmds):
|
for c in sorted(self.console_cmds):
|
||||||
self.console_cmds[c].add_subparser(subparsers)
|
self.console_cmds[c].add_subparser(subparsers)
|
||||||
|
|
||||||
def start(self, args=None):
|
def start(self):
|
||||||
super(Console, self).start(args)
|
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)
|
from deluge.ui.console.main import ConsoleUI # import here because (see top)
|
||||||
|
|
||||||
def run(options):
|
def run(options):
|
||||||
|
|
|
@ -24,6 +24,7 @@ from deluge.error import DelugeError
|
||||||
from deluge.ui.client import client
|
from deluge.ui.client import client
|
||||||
from deluge.ui.console import colors
|
from deluge.ui.console import colors
|
||||||
from deluge.ui.console.colors import ConsoleColorFormatter
|
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.eventlog import EventLog
|
||||||
from deluge.ui.console.statusbars import StatusBars
|
from deluge.ui.console.statusbars import StatusBars
|
||||||
from deluge.ui.coreconfig import CoreConfig
|
from deluge.ui.coreconfig import CoreConfig
|
||||||
|
@ -49,6 +50,58 @@ class ConsoleCommandParser(argparse.ArgumentParser):
|
||||||
self.epilog = epilog
|
self.epilog = epilog
|
||||||
return help_str
|
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):
|
class OptionParser(ConsoleCommandParser):
|
||||||
|
|
||||||
|
@ -130,6 +183,7 @@ class BaseCommand(object):
|
||||||
opts["usage"] = self.usage
|
opts["usage"] = self.usage
|
||||||
parser = OptionParser(**opts)
|
parser = OptionParser(**opts)
|
||||||
parser.add_argument(self.name, metavar="")
|
parser.add_argument(self.name, metavar="")
|
||||||
|
parser.base_parser = parser
|
||||||
self.add_arguments(parser)
|
self.add_arguments(parser)
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
@ -167,8 +221,7 @@ class ConsoleUI(component.Component):
|
||||||
# Set the interactive flag to indicate where we should print the output
|
# Set the interactive flag to indicate where we should print the output
|
||||||
self.interactive = True
|
self.interactive = True
|
||||||
self._commands = cmds
|
self._commands = cmds
|
||||||
|
if options.parsed_cmds:
|
||||||
if options.remaining:
|
|
||||||
self.interactive = False
|
self.interactive = False
|
||||||
if not cmds:
|
if not cmds:
|
||||||
print("Sorry, couldn't find any commands")
|
print("Sorry, couldn't find any commands")
|
||||||
|
@ -194,14 +247,6 @@ Please use commands from the command line, e.g.:\n
|
||||||
reactor.run()
|
reactor.run()
|
||||||
|
|
||||||
def exec_args(self, options):
|
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)
|
commander = Commander(self._commands)
|
||||||
|
|
||||||
def on_connect(result):
|
def on_connect(result):
|
||||||
|
@ -209,11 +254,14 @@ Please use commands from the command line, e.g.:\n
|
||||||
def on_started(result):
|
def on_started(result):
|
||||||
def do_command(result, cmd):
|
def do_command(result, cmd):
|
||||||
return commander.do_command(cmd)
|
return commander.do_command(cmd)
|
||||||
|
|
||||||
|
def exec_command(result, cmd):
|
||||||
|
return commander.exec_command(cmd)
|
||||||
d = defer.succeed(None)
|
d = defer.succeed(None)
|
||||||
for command in commands:
|
for command in options.parsed_cmds:
|
||||||
if command in ("quit", "exit"):
|
if command.command in ("quit", "exit"):
|
||||||
break
|
break
|
||||||
d.addCallback(do_command, command)
|
d.addCallback(exec_command, command)
|
||||||
d.addCallback(do_command, "quit")
|
d.addCallback(do_command, "quit")
|
||||||
|
|
||||||
# We need to wait for the rpcs in start() to finish before processing
|
# 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")
|
commander.do_command("quit")
|
||||||
|
|
||||||
d = None
|
d = None
|
||||||
if not self.interactive:
|
if not self.interactive and options.parsed_cmds[0].command == "connect":
|
||||||
if commands[0] is not None:
|
d = commander.do_command(options.parsed_cmds.pop(0))
|
||||||
if commands[0].startswith("connect"):
|
else:
|
||||||
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:
|
|
||||||
log.info("connect: host=%s, port=%s, username=%s, password=%s",
|
log.info("connect: host=%s, port=%s, username=%s, password=%s",
|
||||||
options.daemon_addr, options.daemon_port, options.daemon_user, options.daemon_pass)
|
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)
|
d = client.connect(options.daemon_addr, options.daemon_port, options.daemon_user, options.daemon_pass)
|
||||||
|
|
|
@ -27,8 +27,8 @@ class Gtk(UI):
|
||||||
help=_("Add one or more torrent files, torrent URLs or magnet URIs"
|
help=_("Add one or more torrent files, torrent URLs or magnet URIs"
|
||||||
" to a currently running Deluge GTK instance"))
|
" to a currently running Deluge GTK instance"))
|
||||||
|
|
||||||
def start(self, args=None):
|
def start(self):
|
||||||
super(Gtk, self).start(args)
|
super(Gtk, self).start()
|
||||||
from deluge.ui.gtkui.gtkui import GtkUI
|
from deluge.ui.gtkui.gtkui import GtkUI
|
||||||
import deluge.common
|
import deluge.common
|
||||||
|
|
||||||
|
|
|
@ -25,16 +25,20 @@ except ImportError:
|
||||||
|
|
||||||
|
|
||||||
class UI(object):
|
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.__name = name
|
||||||
|
self.ui_args = kwargs.pop("ui_args", None)
|
||||||
lang.setup_translations(setup_pygtk=(name == "gtk"))
|
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):
|
def parse_args(self, parser, args=None):
|
||||||
options = self.parser.parse_args(args)
|
options = parser.parse_args(args)
|
||||||
if not hasattr(options, "remaining"):
|
if not hasattr(options, "remaining"):
|
||||||
options.remaining = []
|
options.remaining = []
|
||||||
return options
|
return options
|
||||||
|
@ -51,12 +55,11 @@ class UI(object):
|
||||||
def options(self):
|
def options(self):
|
||||||
return self.__options
|
return self.__options
|
||||||
|
|
||||||
def start(self, extra_args=None):
|
def start(self, parser=None):
|
||||||
args = deluge.common.unicode_argv()[1:]
|
args = deluge.common.unicode_argv()[1:]
|
||||||
if extra_args:
|
if parser is None:
|
||||||
args.extend(extra_args)
|
parser = self.parser
|
||||||
|
self.__options = self.parse_args(parser, args)
|
||||||
self.__options = self.parse_args(args)
|
|
||||||
|
|
||||||
setproctitle("deluge-%s" % self.__name)
|
setproctitle("deluge-%s" % self.__name)
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,9 @@
|
||||||
|
|
||||||
"""Main starting point for Deluge"""
|
"""Main starting point for Deluge"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
@ -32,27 +34,29 @@ def start_ui():
|
||||||
"""Entry point for ui script"""
|
"""Entry point for ui script"""
|
||||||
lang.setup_translations()
|
lang.setup_translations()
|
||||||
|
|
||||||
# Setup the argument parser
|
# Get the registered UI entry points
|
||||||
parser = BaseArgParser()
|
|
||||||
group = parser.add_argument_group(_("UI Options"))
|
|
||||||
|
|
||||||
ui_entrypoints = dict([(entrypoint.name, entrypoint.load())
|
ui_entrypoints = dict([(entrypoint.name, entrypoint.load())
|
||||||
for entrypoint in pkg_resources.iter_entry_points("deluge.ui")])
|
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:")]
|
def add_ui_options_group(_parser):
|
||||||
cmd_help.extend(["%s -- %s" % (k, getattr(v, "cmd_description", "")) for k, v in ui_entrypoints.iteritems()])
|
"""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",
|
# Setup parser with Common Options and add UI Options group.
|
||||||
choices=ui_entrypoints.keys(), help="\n * ".join(cmd_help))
|
parser = add_ui_options_group(BaseArgParser())
|
||||||
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")
|
|
||||||
|
|
||||||
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)
|
config = deluge.configmanager.ConfigManager("ui.conf", DEFAULT_PREFS)
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
log.info("Deluge ui %s", deluge.common.get_version())
|
||||||
|
|
||||||
if options.default_ui:
|
if options.default_ui:
|
||||||
config["default_ui"] = 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)
|
log.info("The default UI has been changed to %s", options.default_ui)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
log.info("Deluge ui %s", deluge.common.get_version())
|
default_ui = config["default_ui"]
|
||||||
log.debug("options: %s", options)
|
config.save() # Save in case config didn't already exist.
|
||||||
|
|
||||||
selected_ui = options.ui if options.ui else config["default_ui"]
|
|
||||||
|
|
||||||
config.save()
|
|
||||||
del config
|
del config
|
||||||
|
|
||||||
# reconstruct arguments to hand off to child client
|
# We have parsed and got the config dir needed to get the default UI
|
||||||
client_args = []
|
# Now create a parser for choosing the UI. We reuse the ui option group for
|
||||||
if options.args:
|
# parsing to succeed and the text displayed to user, but result is not used.
|
||||||
import shlex
|
parser = add_ui_options_group(BaseArgParser(common_help=True))
|
||||||
client_args.extend(shlex.split(options.args))
|
|
||||||
|
# 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:
|
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:
|
except KeyError as ex:
|
||||||
log.error("Unable to find the requested UI: '%s'. Please select a different UI with the '-u' option"
|
log.error("Unable to find chosen UI: '%s'. Please choose a different UI "
|
||||||
" or alternatively use the '-s' option to select a different default UI.", selected_ui)
|
"or use '--set-default-ui' to change default UI.", selected_ui)
|
||||||
except ImportError as ex:
|
except ImportError as ex:
|
||||||
import traceback
|
import traceback
|
||||||
error_type, error_value, tb = sys.exc_info()
|
error_type, error_value, tb = sys.exc_info()
|
||||||
stack = traceback.extract_tb(tb)
|
stack = traceback.extract_tb(tb)
|
||||||
last_frame = stack[-1]
|
last_frame = stack[-1]
|
||||||
if last_frame[0] == __file__:
|
if last_frame[0] == __file__:
|
||||||
log.error("Unable to find the requested UI: %s. Please select a different UI with the '-u' "
|
log.error("Unable to find chosen UI: '%s'. Please choose a different UI "
|
||||||
"option or alternatively use the '-s' option to select a different default UI.", selected_ui)
|
"or use '--set-default-ui' to change default UI.", selected_ui)
|
||||||
else:
|
else:
|
||||||
log.exception(ex)
|
log.exception(ex)
|
||||||
log.error("There was an error whilst launching the request UI: %s", selected_ui)
|
log.error("Encountered an error launching the request UI: %s", selected_ui)
|
||||||
log.error("Look at the traceback above for more information.")
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
ui.start(client_args)
|
ui.start()
|
||||||
|
|
|
@ -42,8 +42,8 @@ class Web(UI):
|
||||||
def server(self):
|
def server(self):
|
||||||
return self.__server
|
return self.__server
|
||||||
|
|
||||||
def start(self, args=None):
|
def start(self):
|
||||||
super(Web, self).start(args)
|
super(Web, self).start()
|
||||||
|
|
||||||
from deluge.ui.web import server
|
from deluge.ui.web import server
|
||||||
self.__server = server.DelugeWeb(options=self.options)
|
self.__server = server.DelugeWeb(options=self.options)
|
||||||
|
|
Loading…
Reference in New Issue