From aa82efd4f1789436b934be99e118e40971f4cc52 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Sat, 19 Nov 2011 20:20:43 +1100 Subject: [PATCH] [#1974] [UI] Decouple UI selection from core. Add entry points into setup for each of the UIs and then use this information to determine which client UI to run. This ensures that custom UIs may be written and run without the need to modifify deluge source code. --- deluge/main.py | 40 ++++++------- deluge/ui/console/__init__.py | 7 ++- deluge/ui/console/console.py | 104 ++++++++++++++++++++++++++++++++++ deluge/ui/console/main.py | 87 +--------------------------- deluge/ui/gtkui/__init__.py | 6 +- deluge/ui/gtkui/gtkui.py | 36 ++++++------ deluge/ui/ui.py | 2 +- deluge/ui/web/__init__.py | 6 +- deluge/ui/web/web.py | 5 +- setup.py | 7 ++- 10 files changed, 166 insertions(+), 134 deletions(-) create mode 100644 deluge/ui/console/console.py diff --git a/deluge/main.py b/deluge/main.py index 146dcdb14..565afa581 100644 --- a/deluge/main.py +++ b/deluge/main.py @@ -20,6 +20,8 @@ import os import sys from logging import FileHandler, getLogger +import pkg_resources + import deluge.common import deluge.configmanager import deluge.error @@ -38,11 +40,14 @@ def start_ui(): parser = CommonOptionParser() group = optparse.OptionGroup(parser, _("Default Options")) - group.add_option("-u", "--ui", dest="ui", - help="""The UI that you wish to launch. The UI choices are:\n - \t gtk -- A GTK-based graphical user interface (default)\n - \t web -- A web-based interface (http://localhost:8112)\n - \t console -- A console or command-line interface""", action="store", type="str") + ui_entrypoints = dict([(entrypoint.name, entrypoint.load()) + for entrypoint in pkg_resources.iter_entry_points('deluge.ui')]) + + cmd_help = ['The UI that you wish to launch. The UI choices are:'] + cmd_help.extend(["%s -- %s" % (k, getattr(v, 'cmdline', "")) for k, v in ui_entrypoints.iteritems()]) + + parser.add_option("-u", "--ui", dest="ui", + choices=ui_entrypoints.keys(), help="\n\t ".join(cmd_help), action="store") group.add_option("-a", "--args", dest="args", help="Arguments to pass to UI, -a '--option args'", action="store", type="str") group.add_option("-s", "--set-default-ui", dest="default_ui", @@ -79,22 +84,11 @@ def start_ui(): client_args.extend(args) try: - if selected_ui == "gtk": - log.info("Starting GtkUI..") - from deluge.ui.gtkui.gtkui import Gtk - ui = Gtk(skip_common=True) - ui.start(client_args) - elif selected_ui == "web": - log.info("Starting WebUI..") - from deluge.ui.web.web import Web - ui = Web(skip_common=True) - ui.start(client_args) - elif selected_ui == "console": - log.info("Starting ConsoleUI..") - from deluge.ui.console.main import Console - ui = Console(skip_common=True) - ui.start(client_args) - except ImportError, e: + ui = ui_entrypoints[selected_ui](skip_common=True) + 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) + except ImportError as ex: import sys import traceback error_type, error_value, tb = sys.exc_info() @@ -104,10 +98,12 @@ def start_ui(): 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) else: - log.exception(e) + 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.") sys.exit(1) + else: + ui.start(client_args) def start_daemon(skip_start=False): diff --git a/deluge/ui/console/__init__.py b/deluge/ui/console/__init__.py index f5e7afd79..79b91e487 100644 --- a/deluge/ui/console/__init__.py +++ b/deluge/ui/console/__init__.py @@ -8,6 +8,9 @@ # UI_PATH = __path__[0] -from deluge.ui.console.main import start # NOQA -assert start # silence pyflakes +from deluge.ui.console.console import Console + + +def start(): + Console().start() diff --git a/deluge/ui/console/console.py b/deluge/ui/console/console.py new file mode 100644 index 000000000..096b46454 --- /dev/null +++ b/deluge/ui/console/console.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2008-2009 Ido Abramovich +# Copyright (C) 2009 Andrew Resch +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +import optparse +import os +import sys + +from deluge.ui.console import UI_PATH +from deluge.ui.ui import UI + + +def load_commands(command_dir, exclude=[]): + def get_command(name): + return getattr(__import__('deluge.ui.console.commands.%s' % name, {}, {}, ['Command']), 'Command')() + + try: + commands = [] + for filename in os.listdir(command_dir): + if filename.split('.')[0] in exclude or filename.startswith('_'): + continue + if not (filename.endswith('.py') or filename.endswith('.pyc')): + continue + cmd = get_command(filename.split('.')[len(filename.split('.')) - 2]) + aliases = [filename.split('.')[len(filename.split('.')) - 2]] + aliases.extend(cmd.aliases) + for a in aliases: + commands.append((a, cmd)) + return dict(commands) + except OSError: + return {} + + +class Console(UI): + + help = """Starts the Deluge console interface""" + cmdline = """A console or command-line interface""" + + def __init__(self, *args, **kwargs): + super(Console, self).__init__("console", *args, **kwargs) + group = optparse.OptionGroup(self.parser, "Console Options", "These options control how " + "the console connects to the daemon. These options will be " + "used if you pass a command, or if you have autoconnect " + "enabled for the console ui.") + + group.add_option("-d", "--daemon", dest="daemon_addr", + action="store", type="str", default="127.0.0.1", + help="Set the address of the daemon to connect to. [default: %default]") + group.add_option("-p", "--port", dest="daemon_port", + help="Set the port to connect to the daemon on. [default: %default]", + action="store", type="int", default=58846) + group.add_option("-u", "--username", dest="daemon_user", + help="Set the username to connect to the daemon with. [default: %default]", + action="store", type="string") + group.add_option("-P", "--password", dest="daemon_pass", + help="Set the password to connect to the daemon with. [default: %default]", + action="store", type="string") + self.parser.add_option_group(group) + + self.cmds = load_commands(os.path.join(UI_PATH, 'commands')) + + class CommandOptionGroup(optparse.OptionGroup): + def __init__(self, parser, title, description=None, cmds=None): + optparse.OptionGroup.__init__(self, parser, title, description) + self.cmds = cmds + + def format_help(self, formatter): + result = formatter.format_heading(self.title) + formatter.indent() + if self.description: + result += "%s\n" % formatter.format_description(self.description) + for cname in self.cmds: + cmd = self.cmds[cname] + if cmd.interactive_only or cname in cmd.aliases: + continue + allnames = [cname] + allnames.extend(cmd.aliases) + cname = "/".join(allnames) + result += formatter.format_heading(" - ".join([cname, cmd.__doc__])) + formatter.indent() + result += "%*s%s\n" % (formatter.current_indent, "", cmd.usage) + formatter.dedent() + formatter.dedent() + return result + cmd_group = CommandOptionGroup(self.parser, "Console Commands", + description="The following commands can be issued at the " + "command line. Commands should be quoted, so, for example, " + "to pause torrent with id 'abc' you would run: '%s " + "\"pause abc\"'" % os.path.basename(sys.argv[0]), + cmds=self.cmds) + self.parser.add_option_group(cmd_group) + + def start(self, args=None): + from main import ConsoleUI + super(Console, self).start(args) + ConsoleUI(self.args, self.cmds, (self.options.daemon_addr, + self.options.daemon_port, self.options.daemon_user, + self.options.daemon_pass)) diff --git a/deluge/ui/console/main.py b/deluge/ui/console/main.py index 2aa981ebe..e75731a9d 100644 --- a/deluge/ui/console/main.py +++ b/deluge/ui/console/main.py @@ -13,7 +13,6 @@ from __future__ import print_function import locale import logging import optparse -import os import re import shlex import sys @@ -23,75 +22,15 @@ from twisted.internet import defer, reactor import deluge.common import deluge.component as component from deluge.ui.client import client -from deluge.ui.console import UI_PATH, colors +from deluge.ui.console import colors from deluge.ui.console.eventlog import EventLog from deluge.ui.console.statusbars import StatusBars from deluge.ui.coreconfig import CoreConfig from deluge.ui.sessionproxy import SessionProxy -from deluge.ui.ui import _UI log = logging.getLogger(__name__) -class Console(_UI): - - help = """Starts the Deluge console interface""" - - def __init__(self, *args, **kwargs): - super(Console, self).__init__("console", *args, **kwargs) - group = optparse.OptionGroup(self.parser, "Console Options", "These daemon connect options will be " - "used for commands, or if console ui autoconnect is enabled.") - group.add_option("-d", "--daemon", dest="daemon_addr") - group.add_option("-p", "--port", dest="daemon_port", type="int") - group.add_option("-u", "--username", dest="daemon_user") - group.add_option("-P", "--password", dest="daemon_pass") - - self.parser.add_option_group(group) - self.parser.disable_interspersed_args() - - self.console_cmds = load_commands(os.path.join(UI_PATH, "commands")) - - class CommandOptionGroup(optparse.OptionGroup): - def __init__(self, parser, title, description=None, cmds=None): - optparse.OptionGroup.__init__(self, parser, title, description) - self.cmds = cmds - - def format_help(self, formatter): - result = formatter.format_heading(self.title) - formatter.indent() - if self.description: - result += "%s\n" % formatter.format_description(self.description) - for cname in self.cmds: - cmd = self.cmds[cname] - if cmd.interactive_only or cname in cmd.aliases: - continue - allnames = [cname] - allnames.extend(cmd.aliases) - cname = "/".join(allnames) - result += formatter.format_heading(" - ".join([cname, cmd.__doc__])) - formatter.indent() - result += "%*s%s\n" % (formatter.current_indent, "", cmd.usage.split("\n")[0]) - formatter.dedent() - formatter.dedent() - return result - cmd_group = CommandOptionGroup(self.parser, "Console Commands", - description="""These commands can be issued from the command line. - They require quoting and multiple commands separated by ';' - e.g. Pause torrent with id 'abcd' and get information for id 'efgh': - `%s \"pause abcd; info efgh\"`""" - % os.path.basename(sys.argv[0]), cmds=self.console_cmds) - self.parser.add_option_group(cmd_group) - - def start(self, args=None): - super(Console, self).start(args) - ConsoleUI(self.args, self.console_cmds, (self.options.daemon_addr, self.options.daemon_port, - self.options.daemon_user, self.options.daemon_pass)) - - -def start(): - Console().start() - - class DelugeHelpFormatter(optparse.IndentedHelpFormatter): """ Format help in a way suited to deluge Legacy mode - colors, format, indentation... @@ -247,30 +186,6 @@ class BaseCommand(object): return OptionParser(prog=self.name, usage=self.usage, epilog=self.epilog, option_list=self.option_list) -def load_commands(command_dir, exclude=None): - if not exclude: - exclude = [] - - def get_command(name): - return getattr(__import__("deluge.ui.console.commands.%s" % name, {}, {}, ["Command"]), "Command")() - - try: - commands = [] - for filename in os.listdir(command_dir): - if filename.split(".")[0] in exclude or filename.startswith("_"): - continue - if not (filename.endswith(".py") or filename.endswith(".pyc")): - continue - cmd = get_command(filename.split(".")[len(filename.split(".")) - 2]) - aliases = [filename.split(".")[len(filename.split(".")) - 2]] - aliases.extend(cmd.aliases) - for a in aliases: - commands.append((a, cmd)) - return dict(commands) - except OSError: - return {} - - class ConsoleUI(component.Component): def __init__(self, args=None, cmds=None, daemon=None): component.Component.__init__(self, "ConsoleUI", 2) diff --git a/deluge/ui/gtkui/__init__.py b/deluge/ui/gtkui/__init__.py index b7eb0288f..709fb8e6a 100644 --- a/deluge/ui/gtkui/__init__.py +++ b/deluge/ui/gtkui/__init__.py @@ -1,3 +1,5 @@ -from deluge.ui.gtkui.gtkui import start +from deluge.ui.gtkui.gtkui import Gtk -assert start # silence pyflakes + +def start(): + Gtk().start() diff --git a/deluge/ui/gtkui/gtkui.py b/deluge/ui/gtkui/gtkui.py index 53698b31c..a1b6f4040 100644 --- a/deluge/ui/gtkui/gtkui.py +++ b/deluge/ui/gtkui/gtkui.py @@ -52,7 +52,7 @@ from deluge.ui.gtkui.torrentdetails import TorrentDetails from deluge.ui.gtkui.torrentview import TorrentView from deluge.ui.sessionproxy import SessionProxy from deluge.ui.tracker_icons import TrackerIcons -from deluge.ui.ui import _UI +from deluge.ui.ui import UI gobject.set_prgname("deluge") @@ -69,21 +69,6 @@ except ImportError: return -class Gtk(_UI): - - help = """Starts the Deluge GTK+ interface""" - - def __init__(self, *args, **kwargs): - super(Gtk, self).__init__("gtk", *args, **kwargs) - - def start(self, args=None): - super(Gtk, self).start(args) - GtkUI(self.args) - - -def start(): - Gtk().start() - DEFAULT_PREFS = { "classic_mode": True, "interactive_add": True, @@ -146,6 +131,25 @@ DEFAULT_PREFS = { } +class Gtk(UI): + + help = """Starts the Deluge GTK+ interface""" + cmdline = """A GTK-based graphical user interface""" + + def __init__(self, *args, **kwargs): + super(Gtk, self).__init__("gtk", *args, **kwargs) + + group = self.parser.add_argument_group(_('GTK Options')) + group.add_argument("torrents", metavar="", nargs="*", default=None, + help="Add one or more torrent files, torrent URLs or magnet URIs" + " to a currently running Deluge GTK instance") + + def start(self, args=None): + from gtkui import GtkUI + super(Gtk, self).start(args) + GtkUI(self.options) + + class GtkUI(object): def __init__(self, args): # Setup gtkbuilder/glade translation diff --git a/deluge/ui/ui.py b/deluge/ui/ui.py index 828d763f4..2e53aefe3 100644 --- a/deluge/ui/ui.py +++ b/deluge/ui/ui.py @@ -30,7 +30,7 @@ if 'dev' not in deluge.common.get_version(): warnings.filterwarnings('ignore', category=DeprecationWarning, module='twisted') -class _UI(object): +class UI(object): def __init__(self, name="gtk", skip_common=False): self.__name = name diff --git a/deluge/ui/web/__init__.py b/deluge/ui/web/__init__.py index 81070096c..99d896ba3 100644 --- a/deluge/ui/web/__init__.py +++ b/deluge/ui/web/__init__.py @@ -1,3 +1,5 @@ -from deluge.ui.web.web import start +from deluge.ui.web.web import Web -assert start # silence pyflakes + +def start(): + Web().start() diff --git a/deluge/ui/web/web.py b/deluge/ui/web/web.py index 633c104bc..be335c468 100644 --- a/deluge/ui/web/web.py +++ b/deluge/ui/web/web.py @@ -14,7 +14,7 @@ from optparse import OptionGroup from deluge.common import osx_check, windows_check from deluge.configmanager import get_config_dir -from deluge.ui.ui import _UI +from deluge.ui.ui import UI class WebUI(object): @@ -24,9 +24,10 @@ class WebUI(object): deluge_web.start() -class Web(_UI): +class Web(UI): help = """Starts the Deluge web interface""" + cmdline = """A web-based interface (http://localhost:8112)""" def __init__(self, *args, **kwargs): super(Web, self).__init__("web", *args, **kwargs) diff --git a/setup.py b/setup.py index fa7408b4a..a5c986b4c 100755 --- a/setup.py +++ b/setup.py @@ -310,7 +310,12 @@ entry_points = { 'gui_scripts': [ 'deluge = deluge.main:start_ui', 'deluge-gtk = deluge.ui.gtkui:start' - ] + ], + 'deluge.ui': [ + 'console = deluge.ui.console:Console', + 'web = deluge.ui.web:Web', + 'gtk = deluge.ui.gtkui:Gtk', + ], } if windows_check():