From 7b54a2a1ee999b9264ad5b8b5c2c6c51823003ba Mon Sep 17 00:00:00 2001 From: bendikro Date: Mon, 11 Jan 2016 18:12:14 +0100 Subject: [PATCH] [UI] Replace optparse with argparse for cmd arguments handling optparse is deprecation and succeeded by argparse. See https://www.python.org/dev/peps/pep-0389 --- deluge/common.py | 22 ++ deluge/commonoptions.py | 79 ------- deluge/main.py | 89 ++++---- deluge/scripts/create_plugin.py | 75 +++---- deluge/ui/baseargparser.py | 137 ++++++++++++ deluge/ui/client.py | 27 ++- deluge/ui/console/__init__.py | 2 +- deluge/ui/console/colors.py | 38 ++++ deluge/ui/console/commander.py | 79 ++----- deluge/ui/console/commands/add.py | 40 ++-- deluge/ui/console/commands/cache.py | 3 +- deluge/ui/console/commands/config.py | 40 ++-- deluge/ui/console/commands/connect.py | 33 ++- deluge/ui/console/commands/debug.py | 10 +- deluge/ui/console/commands/gui.py | 10 +- deluge/ui/console/commands/halt.py | 3 +- deluge/ui/console/commands/help.py | 24 +- deluge/ui/console/commands/info.py | 70 +++--- deluge/ui/console/commands/manage.py | 44 ++-- deluge/ui/console/commands/move.py | 37 ++-- deluge/ui/console/commands/pause.py | 17 +- deluge/ui/console/commands/plugin.py | 71 ++---- deluge/ui/console/commands/quit.py | 4 +- deluge/ui/console/commands/recheck.py | 14 +- deluge/ui/console/commands/resume.py | 19 +- deluge/ui/console/commands/rm.py | 19 +- deluge/ui/console/commands/status.py | 54 ++--- deluge/ui/console/commands/update_tracker.py | 14 +- deluge/ui/console/console.py | 95 +++----- deluge/ui/console/main.py | 222 ++++++++++--------- deluge/ui/console/modes/alltorrents.py | 3 - deluge/ui/console/modes/connectionmanager.py | 2 + deluge/ui/console/modes/legacy.py | 68 +----- deluge/ui/gtkui/gtkui.py | 5 +- deluge/ui/ui.py | 41 ++-- deluge/ui/web/web.py | 62 +++--- 36 files changed, 783 insertions(+), 789 deletions(-) delete mode 100644 deluge/commonoptions.py create mode 100644 deluge/ui/baseargparser.py diff --git a/deluge/common.py b/deluge/common.py index 2a380efbc..b56b383b6 100644 --- a/deluge/common.py +++ b/deluge/common.py @@ -162,6 +162,28 @@ def osx_check(): return platform.system() == "Darwin" +def linux_check(): + """ + Checks if the current platform is Linux + + :returns: True or False + :rtype: bool + + """ + return platform.system() == "Linux" + + +def get_os_version(): + if windows_check(): + return platform.win32_ver() + elif osx_check(): + return platform.mac_ver() + elif linux_check(): + return platform.linux_distribution() + else: + return (platform.release(), ) + + def get_pixmap(fname): """ Provides easy access to files in the deluge/ui/data/pixmaps folder within the Deluge egg diff --git a/deluge/commonoptions.py b/deluge/commonoptions.py deleted file mode 100644 index 99e60f897..000000000 --- a/deluge/commonoptions.py +++ /dev/null @@ -1,79 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2007 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 logging -import optparse -import os -import platform -import sys - -import deluge.common -import deluge.configmanager -from deluge.log import setup_logger - - -def version_callback(option, opt_str, value, parser): - print(os.path.basename(sys.argv[0]) + ": " + deluge.common.get_version()) - try: - from deluge._libtorrent import lt - print("libtorrent: %s" % lt.version) - except ImportError: - pass - print("Python: %s" % platform.python_version()) - for version in (platform.linux_distribution(), platform.win32_ver(), platform.mac_ver(), ("n/a",)): - if filter(None, version): # pylint: disable=bad-builtin - print("OS: %s %s" % (platform.system(), " ".join(version))) - break - raise SystemExit - - -class CommonOptionParser(optparse.OptionParser): - def __init__(self, *args, **kwargs): - optparse.OptionParser.__init__(self, *args, **kwargs) - self.common_group = optparse.OptionGroup(self, _("Common Options")) - self.common_group.add_option("-v", "--version", action="callback", callback=version_callback, - help="Show program's version number and exit") - self.common_group.add_option("-c", "--config", dest="config", action="store", type="str", - help="Set the config folder location") - self.common_group.add_option("-l", "--logfile", dest="logfile", action="store", type="str", - help="Output to designated logfile instead of stdout") - self.common_group.add_option("-L", "--loglevel", dest="loglevel", action="store", type="str", - help="Set the log level: none, info, warning, error, critical, debug") - self.common_group.add_option("-q", "--quiet", dest="quiet", action="store_true", default=False, - help="Sets the log level to 'none', this is the same as `-L none`") - self.common_group.add_option("-r", "--rotate-logs", action="store_true", default=False, - help="Rotate logfiles.") - self.add_option_group(self.common_group) - - def parse_args(self, *args): - options, args = optparse.OptionParser.parse_args(self, *args) - - # Setup the logger - if options.quiet: - options.loglevel = "none" - if options.loglevel: - options.loglevel = options.loglevel.lower() - - logfile_mode = 'w' - if options.rotate_logs: - logfile_mode = 'a' - - # Setup the logger - setup_logger(level=options.loglevel, filename=options.logfile, filemode=logfile_mode) - - if options.config: - if not deluge.configmanager.set_config_dir(options.config): - log = logging.getLogger(__name__) - log.error("There was an error setting the config dir! Exiting..") - sys.exit(1) - else: - if not os.path.exists(deluge.common.get_default_config_dir()): - os.makedirs(deluge.common.get_default_config_dir()) - - return options, args diff --git a/deluge/main.py b/deluge/main.py index 565afa581..909b47173 100644 --- a/deluge/main.py +++ b/deluge/main.py @@ -15,7 +15,6 @@ """Main starting point for Deluge. Contains the main() entry point.""" from __future__ import print_function -import optparse import os import sys from logging import FileHandler, getLogger @@ -38,25 +37,27 @@ def start_ui(): # Setup the argument parser parser = CommonOptionParser() - group = optparse.OptionGroup(parser, _("Default Options")) + group = parser.add_argument_group(_("Default Options")) 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:'] + max_len = 0 + for k, v in ui_entrypoints.iteritems(): + cmdline = getattr(v, 'cmdline', "") + max_len = max(max_len, len(cmdline)) + 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", - help="Sets the default UI to be run when no UI is specified", action="store", type="str") + parser.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") - parser.add_option_group(group) - - # Get the options and args from the OptionParser - (options, args) = parser.parse_args(deluge.common.unicode_argv()[1:]) + options = parser.parse_args(deluge.common.unicode_argv()[1:]) config = deluge.configmanager.ConfigManager("ui.conf", DEFAULT_PREFS) log = getLogger(__name__) @@ -69,7 +70,6 @@ def start_ui(): log.info("Deluge ui %s", deluge.common.get_version()) log.debug("options: %s", options) - log.debug("args: %s", args) selected_ui = options.ui if options.ui else config["default_ui"] @@ -81,15 +81,13 @@ def start_ui(): if options.args: import shlex client_args.extend(shlex.split(options.args)) - client_args.extend(args) try: - ui = ui_entrypoints[selected_ui](skip_common=True) + ui = ui_entrypoints[selected_ui](parser=parser) 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() stack = traceback.extract_tb(tb) @@ -106,6 +104,31 @@ def start_ui(): ui.start(client_args) +def add_daemon_options(parser): + group = parser.add_argument_group('Daemon Options') + group.add_argument("-p", "--port", metavar="", action="store", type=int, + help="The port the daemon will listen on") + group.add_argument("-i", "--interface", metavar="", dest="listen_interface", + help="Interface daemon will listen for bittorrent connections on, " + "this should be an IP address", action="store") + group.add_argument("-u", "--ui-interface", metavar="", action="store", + help="Interface daemon will listen for UI connections on, " + "this should be an IP address") + if not deluge.common.windows_check(): + group.add_argument("-d", "--do-not-daemonize", dest="donot", + help="Do not daemonize", action="store_true", default=False) + group.add_argument("-P", "--pidfile", metavar="", + help="Use pidfile to store process id", action="store") + if not deluge.common.windows_check(): + group.add_argument("-U", "--user", metavar="", action="store", + help="User to switch to. Only use it when starting as root") + group.add_argument("-g", "--group", metavar="", action="store", + help="Group to switch to. Only use it when starting as root") + group.add_argument("--read-only-config-keys", + help="List of comma-separated config keys that will not be modified by set_config RPC.", + action="store", type=str, default="") + + def start_daemon(skip_start=False): """ Entry point for daemon script @@ -124,38 +147,10 @@ def start_daemon(skip_start=False): warnings.filterwarnings('ignore', category=DeprecationWarning, module='twisted') # Setup the argument parser - parser = CommonOptionParser(usage="%prog [options] [actions]") + parser = CommonOptionParser() + add_daemon_options(parser) - group = optparse.OptionGroup(parser, _("Daemon Options")) - group.add_option("-p", "--port", dest="port", - help="Port daemon will listen on", action="store", type="int") - group.add_option("-i", "--interface", dest="listen_interface", - help="Interface daemon will listen for bittorrent connections on, " - "this should be an IP address", metavar="IFACE", - action="store", type="str") - group.add_option("-u", "--ui-interface", dest="ui_interface", - help="Interface daemon will listen for UI connections on, this should be " - "an IP address", metavar="IFACE", action="store", type="str") - if not (deluge.common.windows_check() or deluge.common.osx_check()): - group.add_option("-d", "--do-not-daemonize", dest="donot", - help="Do not daemonize", action="store_true", default=False) - group.add_option("-P", "--pidfile", dest="pidfile", - help="Use pidfile to store process id", action="store", type="str") - if not deluge.common.windows_check(): - group.add_option("-U", "--user", dest="user", - help="User to switch to. Only use it when starting as root", action="store", type="str") - group.add_option("-g", "--group", dest="group", - help="Group to switch to. Only use it when starting as root", action="store", type="str") - group.add_option("--read-only-config-keys", - help="List of comma-separated config keys that will not be modified by set_config RPC.", - action="store", type="str", default="") - group.add_option("--profile", dest="profile", action="store_true", default=False, - help="Profiles the daemon") - - parser.add_option_group(group) - - # Get the options and args from the OptionParser - (options, args) = parser.parse_args() + options = parser.parse_args() # Check for any daemons running with this same config from deluge.core.daemon import check_running_daemon diff --git a/deluge/scripts/create_plugin.py b/deluge/scripts/create_plugin.py index b8ae99350..16033531c 100644 --- a/deluge/scripts/create_plugin.py +++ b/deluge/scripts/create_plugin.py @@ -10,42 +10,31 @@ from __future__ import print_function import os import sys +from argparse import ArgumentParser from datetime import datetime -from optparse import OptionParser import deluge.common -parser = OptionParser() -parser.add_option("-n", "--name", dest="name", help="plugin name") -parser.add_option("-m", "--module-name", dest="module", help="plugin name") -parser.add_option("-p", "--basepath", dest="path", help="base path") -parser.add_option("-a", "--author-name", dest="author_name", help="author name,for the GPL header") -parser.add_option("-e", "--author-email", dest="author_email", help="author email,for the GPL header") -parser.add_option("-u", "--url", dest="url", help="Homepage URL") -parser.add_option("-c", "--config", dest="configdir", help="location of deluge configuration") +parser = ArgumentParser() +parser.add_argument("-n", "--name", metavar="", required=True, help="Plugin name") +parser.add_argument("-m", "--module-name", metavar="", help="Module name") +parser.add_argument("-p", "--basepath", metavar="", required=True, help="Base path") +parser.add_argument("-a", "--author-name", metavar="", required=True, + help="Author name,for the GPL header") +parser.add_argument("-e", "--author-email", metavar="", required=True, + help="Author email,for the GPL header") +parser.add_argument("-u", "--url", metavar="", help="Homepage URL") +parser.add_argument("-c", "--config", metavar="", dest="configdir", help="Location of deluge configuration") - -(options, args) = parser.parse_args() +options = parser.parse_args() def create_plugin(): - if not options.name: - print("--name is mandatory , use -h for more info") - return - if not options.path: - print("--basepath is mandatory , use -h for more info") - return - if not options.author_email: - print("--author-email is mandatory , use -h for more info") - return - if not options.author_email: - print("--author-name is mandatory , use -h for more info") - return if not options.url: options.url = "" - if not os.path.exists(options.path): + if not os.path.exists(options.basepath): print("basepath does not exist") return @@ -57,9 +46,9 @@ def create_plugin(): real_name = options.name name = real_name.replace(" ", "_") safe_name = name.lower() - if options.module: - safe_name = options.module.lower() - plugin_base = os.path.realpath(os.path.join(options.path, name)) + if options.module_name: + safe_name = options.module_name.lower() + plugin_base = os.path.realpath(os.path.join(options.basepath, name)) deluge_namespace = os.path.join(plugin_base, "deluge") plugins_namespace = os.path.join(deluge_namespace, "plugins") src = os.path.join(plugins_namespace, safe_name) @@ -121,16 +110,16 @@ def create_plugin(): CORE = """ import logging from deluge.plugins.pluginbase import CorePluginBase -import deluge.component as component import deluge.configmanager from deluge.core.rpcserver import export DEFAULT_PREFS = { - "test":"NiNiNi" + "test": "NiNiNi" } log = logging.getLogger(__name__) + class Core(CorePluginBase): def enable(self): self.config = deluge.configmanager.ConfigManager("%(safe_name)s.conf", DEFAULT_PREFS) @@ -157,22 +146,25 @@ class Core(CorePluginBase): INIT = """ from deluge.plugins.init import PluginInitBase + class CorePlugin(PluginInitBase): def __init__(self, plugin_name): - from core import Core as _plugin_cls - self._plugin_cls = _plugin_cls + from core import Core as PluginClass + self._plugin_cls = PluginClass super(CorePlugin, self).__init__(plugin_name) + class GtkUIPlugin(PluginInitBase): def __init__(self, plugin_name): - from gtkui import GtkUI as _plugin_cls - self._plugin_cls = _plugin_cls + from gtkui import GtkUI as PluginClass + self._plugin_cls = PluginClass super(GtkUIPlugin, self).__init__(plugin_name) + class WebUIPlugin(PluginInitBase): def __init__(self, plugin_name): - from webui import WebUI as _plugin_cls - self._plugin_cls = _plugin_cls + from webui import WebUI as PluginClass + self._plugin_cls = PluginClass super(WebUIPlugin, self).__init__(plugin_name) """ @@ -201,8 +193,8 @@ setup( long_description=__long_description__ if __long_description__ else __description__, packages=find_packages(), - namespace_packages = ["deluge", "deluge.plugins"], - package_data = __pkg_data__, + namespace_packages=["deluge", "deluge.plugins"], + package_data=__pkg_data__, entry_points=\"\"\" [deluge.plugin.core] @@ -218,7 +210,8 @@ setup( COMMON = """ def get_resource(filename): - import pkg_resources, os + import pkg_resources + import os return pkg_resources.resource_filename("deluge.plugins.%(safe_name)s", os.path.join("data", filename)) """ @@ -230,12 +223,12 @@ import logging from deluge.ui.client import client from deluge.plugins.pluginbase import GtkPluginBase import deluge.component as component -import deluge.common from common import get_resource log = logging.getLogger(__name__) + class GtkUI(GtkPluginBase): def enable(self): self.glade = gtk.glade.XML(get_resource("config.glade")) @@ -252,7 +245,7 @@ class GtkUI(GtkPluginBase): def on_apply_prefs(self): log.debug("applying prefs for %(name)s") config = { - "test":self.glade.get_widget("txt_test").get_text() + "test": self.glade.get_widget("txt_test").get_text() } client.%(safe_name)s.set_config(config) @@ -296,13 +289,13 @@ GLADE = """ WEBUI = """ import logging from deluge.ui.client import client -from deluge import component from deluge.plugins.pluginbase import WebPluginBase from common import get_resource log = logging.getLogger(__name__) + class WebUI(WebPluginBase): scripts = [get_resource("%(safe_name)s.js")] diff --git a/deluge/ui/baseargparser.py b/deluge/ui/baseargparser.py new file mode 100644 index 000000000..cdca4e973 --- /dev/null +++ b/deluge/ui/baseargparser.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 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 argparse +import logging +import os +import platform +import sys +import textwrap + +import deluge.common +import deluge.configmanager +import deluge.log +from deluge.log import setup_logger + + +def get_version(): + version_str = "%s\n" % (deluge.common.get_version()) + try: + from deluge._libtorrent import lt + version_str += "libtorrent: %s\n" % lt.version + except ImportError: + pass + version_str += "Python: %s\n" % platform.python_version() + version_str += "OS: %s %s\n" % (platform.system(), " ".join(deluge.common.get_os_version())) + return version_str + + +class DelugeTextHelpFormatter(argparse.RawDescriptionHelpFormatter): + """Help message formatter which retains formatting of all help text. + """ + + def _split_lines(self, text, width): + """ + Do not remove whitespaces in string but still wrap text to max width. + Instead of passing the entire text to textwrap.wrap, split and pass each + line instead. This way list formatting is not mangled by textwrap.wrap. + """ + wrapped_lines = [] + for l in text.splitlines(): + wrapped_lines.extend(textwrap.wrap(l, width)) + return wrapped_lines + + def _format_action_invocation(self, action): + """ + Combines the options with comma and displays the argument + value only once instead of after both options. + Instead of: -s , --long-opt + Show : -s, --long-opt + + """ + if not action.option_strings: + metavar, = self._metavar_formatter(action, action.dest)(1) + return metavar + else: + parts = [] + # if the Optional doesn't take a value, format is: + # -s, --long + if action.nargs == 0: + parts.extend(action.option_strings) + + # if the Optional takes a value, format is: + # -s, --long ARGS + else: + default = action.dest.upper() + args_string = self._format_args(action, default) + opt = ", ".join(action.option_strings) + parts.append('%s %s' % (opt, args_string)) + return ', '.join(parts) + + +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() + parser.exit() + + +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) + + self.group = self.add_argument_group('Common Options') + self.group.add_argument('--version', action='version', version='%(prog)s ' + get_version(), + help="Show program's version number and exit") + self.group.add_argument("-c", "--config", action="store", metavar='', + help="Set the config directory path") + self.group.add_argument("-l", "--logfile", action="store", metavar='', + help="Output to designated logfile instead of stdout") + self.group.add_argument("-L", "--loglevel", action="store", choices=deluge.log.levels, metavar='', + help="Set the log level: %s" % ", ".join(deluge.log.levels)) + self.group.add_argument("-q", "--quiet", action="store_true", default=False, + help="Sets the log level to 'none', this is the same as `-L none`") + self.group.add_argument("-r", "--rotate-logs", action="store_true", default=False, + help="Rotate logfiles.") + self.group.add_argument("-h", "--help", action=HelpAction, help='Show this help message and exit') + + def parse_args(self, *args): + options, remaining = super(BaseArgParser, self).parse_known_args(*args) + options.remaining = remaining + + # Setup the logger + if options.quiet: + options.loglevel = "none" + if options.loglevel: + options.loglevel = options.loglevel.lower() + + logfile_mode = 'w' + if options.rotate_logs: + logfile_mode = 'a' + + # Setup the logger + setup_logger(level=options.loglevel, filename=options.logfile, filemode=logfile_mode) + + if options.config: + if not deluge.configmanager.set_config_dir(options.config): + log = logging.getLogger(__name__) + log.error("There was an error setting the config dir! Exiting..") + sys.exit(1) + else: + if not os.path.exists(deluge.common.get_default_config_dir()): + os.makedirs(deluge.common.get_default_config_dir()) + + return options diff --git a/deluge/ui/client.py b/deluge/ui/client.py index e2c6e24ec..70ddda62a 100644 --- a/deluge/ui/client.py +++ b/deluge/ui/client.py @@ -379,14 +379,14 @@ class DaemonSSLProxy(DaemonProxy): def authenticate(self, username, password): log.debug("%s.authenticate: %s", self.__class__.__name__, username) - self.login_deferred = defer.Deferred() + login_deferred = defer.Deferred() d = self.call("daemon.login", username, password, client_version=deluge.common.get_version()) - d.addCallback(self.__on_login, username) - d.addErrback(self.__on_login_fail) - return self.login_deferred + d.addCallbacks(self.__on_login, self.__on_login_fail, callbackArgs=[username, login_deferred], + errbackArgs=[login_deferred]) + return login_deferred - def __on_login(self, result, username): + def __on_login(self, result, username, login_deferred): log.debug("__on_login called: %s %s", username, result) self.username = username self.authentication_level = result @@ -399,11 +399,10 @@ class DaemonSSLProxy(DaemonProxy): self.__on_auth_levels_mappings ) - self.login_deferred.callback(result) + login_deferred.callback(result) - def __on_login_fail(self, result): - log.debug("_on_login_fail(): %s", result.value) - self.login_deferred.errback(result) + def __on_login_fail(self, result, login_deferred): + login_deferred.errback(result) def __on_auth_levels_mappings(self, result): auth_levels_mapping, auth_levels_mapping_reverse = result @@ -549,7 +548,12 @@ class Client(object): d = self._daemon_proxy.connect(host, port) + def on_connected(daemon_version): + log.debug("on_connected. Daemon version: %s", daemon_version) + return daemon_version + def on_connect_fail(reason): + log.debug("on_connect_fail: %s", reason) self.disconnect() return reason @@ -561,11 +565,6 @@ class Client(object): log.debug("Failed to authenticate: %s", reason.value) return reason - def on_connected(daemon_version): - log.debug("Client.connect.on_connected. Daemon version: %s", - daemon_version) - return daemon_version - def authenticate(daemon_version, username, password): if not username and host in ("127.0.0.1", "localhost"): # No username provided and it's localhost, so attempt to get credentials from auth file. diff --git a/deluge/ui/console/__init__.py b/deluge/ui/console/__init__.py index 79b91e487..2680ada5f 100644 --- a/deluge/ui/console/__init__.py +++ b/deluge/ui/console/__init__.py @@ -7,7 +7,7 @@ # See LICENSE for more details. # -UI_PATH = __path__[0] +UI_PATH = __path__[0] # NOQA Ignore 'E402 module level import not at top of file' from deluge.ui.console.console import Console diff --git a/deluge/ui/console/colors.py b/deluge/ui/console/colors.py index 0875735ec..6b9bb9461 100644 --- a/deluge/ui/console/colors.py +++ b/deluge/ui/console/colors.py @@ -7,6 +7,8 @@ # See LICENSE for more details. # +import re + from deluge.ui.console.modes import format_utils try: @@ -228,3 +230,39 @@ def parse_color_string(s, encoding="UTF-8"): # There was no color scheme so we add it with a 0 for white on black ret = [(0, s)] return ret + + +class ConsoleColorFormatter(object): + """ + Format help in a way suited to deluge Legacy mode - colors, format, indentation... + """ + + replace_dict = { + "": "{!green!}%s{!input!}", + "": "{!green!}%s{!input!}", + "": "{!green!}%s{!input!}", + + "": "{!yellow!}%s{!input!}", + "\\.\\.\\.": "{!yellow!}%s{!input!}", + "\\s\\*\\s": "{!blue!}%s{!input!}", + "(?": "{!white!}%s{!input!}", + "[_A-Z]{3,}": "{!cyan!}%s{!input!}", + "": "{!cyan!}%s{!input!}", + "": "{!cyan!}%s{!input!}", + "usage:": "{!info!}%s{!input!}", + "": "{!yellow!}%s{!input!}", + "": "{!green!}%s{!input!}" + + } + + def format_colors(self, string): + def r(repl): + return lambda s: repl % s.group() + for key, replacement in self.replace_dict.items(): + string = re.sub(key, r(replacement), string) + return string diff --git a/deluge/ui/console/commander.py b/deluge/ui/console/commander.py index bb5f40673..e1c59825d 100644 --- a/deluge/ui/console/commander.py +++ b/deluge/ui/console/commander.py @@ -13,43 +13,45 @@ from __future__ import print_function import logging -import sys from twisted.internet import defer -import deluge.component as component -from deluge.error import DelugeError from deluge.ui.client import client from deluge.ui.console.colors import strip_colors log = logging.getLogger(__name__) -class Commander: +class Commander(object): + def __init__(self, cmds, interactive=False): self._commands = cmds - self.console = component.get("ConsoleUI") self.interactive = interactive def write(self, line): print(strip_colors(line)) - def do_command(self, cmd): + def do_command(self, cmd_line): """ Processes a command. :param cmd: str, the command string """ - if not cmd: + if not cmd_line: return - cmd, _, line = cmd.partition(" ") + cmd, _, line = cmd_line.partition(" ") try: parser = self._commands[cmd].create_parser() except KeyError: self.write("{!error!}Unknown command: %s" % cmd) return - args = self._commands[cmd].split(line) + + try: + args = [cmd] + self._commands[cmd].split(line) + except ValueError as ex: + self.write("{!error!}Error parsing command: %s" % ex) + return # Do a little hack here to print 'command --help' properly parser._print_help = parser.print_help @@ -73,14 +75,20 @@ class Commander: return try: - options, args = parser.parse_args(args) + options = parser.parse_args(args=args) except TypeError as ex: self.write("{!error!}Error parsing options: %s" % ex) + import traceback + self.write("%s" % traceback.format_exc()) + return + except Exception as ex: + self.write("{!error!} %s" % ex) + parser.print_help() return - if not getattr(options, "_exit", False): + if not getattr(parser, "_exit", False): try: - ret = self._commands[cmd].handle(*args, **options.__dict__) + ret = self._commands[cmd].handle(options) except Exception as ex: self.write("{!error!} %s" % ex) log.exception(ex) @@ -89,50 +97,3 @@ class Commander: return defer.succeed(True) else: return ret - - def exec_args(self, args, host, port, username, password): - commands = [] - if args: - # Multiple commands split by ";" - commands = [arg.strip() for arg in args.split(";")] - - def on_connect(result): - def on_started(result): - def on_started(result): - def do_command(result, cmd): - return self.do_command(cmd) - d = defer.succeed(None) - for command in commands: - if command in ("quit", "exit"): - break - d.addCallback(do_command, command) - d.addCallback(do_command, "quit") - - # We need to wait for the rpcs in start() to finish before processing - # any of the commands. - self.console.started_deferred.addCallback(on_started) - component.start().addCallback(on_started) - - def on_connect_fail(reason): - if reason.check(DelugeError): - rm = reason.value.message - else: - rm = reason.getErrorMessage() - if host: - print("Could not connect to daemon: %s:%s\n %s" % (host, port, rm)) - else: - print("Could not connect to localhost daemon\n %s" % rm) - self.do_command("quit") - - if host: - d = client.connect(host, port, username, password) - else: - d = client.connect() - if not self.interactive: - if commands[0].startswith("connect"): - d = self.do_command(commands.pop(0)) - elif "help" in commands: - self.do_command("help") - sys.exit(0) - d.addCallback(on_connect) - d.addErrback(on_connect_fail) diff --git a/deluge/ui/console/commands/add.py b/deluge/ui/console/commands/add.py index 3b3db9214..1fa157758 100644 --- a/deluge/ui/console/commands/add.py +++ b/deluge/ui/console/commands/add.py @@ -10,7 +10,6 @@ import base64 import os -from optparse import make_option from urllib import url2pathname from urlparse import urlparse @@ -23,20 +22,19 @@ from deluge.ui.console.main import BaseCommand class Command(BaseCommand): - """Add a torrent""" - option_list = BaseCommand.option_list + ( - make_option('-p', '--path', dest='path', help='download folder for torrent'), - ) + """Add torrents""" - usage = "Usage: add [-p ] [ ...]\n"\ - " arguments can be file paths, URLs or magnet uris" + def add_arguments(self, parser): + parser.add_argument("-p", "--path", dest="path", help="download folder for torrent") + parser.add_argument("torrents", metavar="", nargs="+", + help="One or more torrent files, URLs or magnet URIs") - def handle(self, *args, **options): + def handle(self, options): self.console = component.get("ConsoleUI") t_options = {} - if options["path"]: - t_options["download_location"] = os.path.expanduser(options["path"]) + if options.path: + t_options["download_location"] = os.path.expanduser(options.path) def on_success(result): if not result: @@ -49,22 +47,22 @@ class Command(BaseCommand): # Keep a list of deferreds to make a DeferredList deferreds = [] - for arg in args: - if not arg.strip(): + for torrent in options.torrents: + if not torrent.strip(): continue - if deluge.common.is_url(arg): - self.console.write("{!info!}Attempting to add torrent from url: %s" % arg) - deferreds.append(client.core.add_torrent_url(arg, t_options).addCallback(on_success).addErrback( + if deluge.common.is_url(torrent): + self.console.write("{!info!}Attempting to add torrent from url: %s" % torrent) + deferreds.append(client.core.add_torrent_url(torrent, t_options).addCallback(on_success).addErrback( on_fail)) - elif deluge.common.is_magnet(arg): - self.console.write("{!info!}Attempting to add torrent from magnet uri: %s" % arg) - deferreds.append(client.core.add_torrent_magnet(arg, t_options).addCallback(on_success).addErrback( + elif deluge.common.is_magnet(torrent): + self.console.write("{!info!}Attempting to add torrent from magnet uri: %s" % torrent) + deferreds.append(client.core.add_torrent_magnet(torrent, t_options).addCallback(on_success).addErrback( on_fail)) else: # Just a file - if urlparse(arg).scheme == "file": - arg = url2pathname(urlparse(arg).path) - path = os.path.abspath(os.path.expanduser(arg)) + if urlparse(torrent).scheme == "file": + torrent = url2pathname(urlparse(torrent).path) + path = os.path.abspath(os.path.expanduser(torrent)) if not os.path.exists(path): self.console.write("{!error!}%s doesn't exist!" % path) continue diff --git a/deluge/ui/console/commands/cache.py b/deluge/ui/console/commands/cache.py index 02dad18d1..9d8efb340 100644 --- a/deluge/ui/console/commands/cache.py +++ b/deluge/ui/console/commands/cache.py @@ -14,9 +14,8 @@ from deluge.ui.console.main import BaseCommand class Command(BaseCommand): """Show information about the disk cache""" - usage = "Usage: cache" - def handle(self, *args, **options): + def handle(self, options): self.console = component.get("ConsoleUI") def on_cache_status(status): diff --git a/deluge/ui/console/commands/config.py b/deluge/ui/console/commands/config.py index 5a10bfe6e..d62f9d0b3 100644 --- a/deluge/ui/console/commands/config.py +++ b/deluge/ui/console/commands/config.py @@ -11,7 +11,6 @@ import cStringIO import logging import tokenize -from optparse import make_option import deluge.component as component import deluge.ui.console.colors as colors @@ -66,25 +65,28 @@ def simple_eval(source): class Command(BaseCommand): """Show and set configuration values""" - option_list = BaseCommand.option_list + ( - make_option("-s", "--set", action="store", nargs=2, dest="set", help="set value for key"), - ) - usage = """Usage: config [key1 [key2 ...]]" - config --set key value""" + usage = "config [--set ] [ [...] ]" - def handle(self, *args, **options): + def add_arguments(self, parser): + set_group = parser.add_argument_group("setting a value") + set_group.add_argument("-s", "--set", action="store", metavar="", help="set value for this key") + set_group.add_argument("values", metavar="", nargs="+", help="Value to set") + get_group = parser.add_argument_group("getting values") + get_group.add_argument("keys", metavar="", nargs="*", help="one or more keys separated by space") + + def handle(self, options): self.console = component.get("ConsoleUI") - if options["set"]: - return self._set_config(*args, **options) + if options.set: + return self._set_config(options) else: - return self._get_config(*args, **options) + return self._get_config(options) - def _get_config(self, *args, **options): + def _get_config(self, options): def _on_get_config(config): keys = sorted(config.keys()) s = "" for key in keys: - if args and key not in args: + if key not in options.values: continue color = "{!white,black,bold!}" value = config[key] @@ -107,10 +109,16 @@ class Command(BaseCommand): return client.core.get_config().addCallback(_on_get_config) - def _set_config(self, *args, **options): + def _set_config(self, options): config = component.get("CoreConfig") - key = options["set"][0] - val = simple_eval(options["set"][1] + " " .join(args)) + key = options.set + val = " ".join(options.values) + + try: + val = simple_eval(val) + except SyntaxError as ex: + self.console.write("{!error!}%s" % ex) + return if key not in config.keys(): self.console.write("{!error!}The key '%s' is invalid!" % key) @@ -126,7 +134,7 @@ class Command(BaseCommand): def on_set_config(result): self.console.write("{!success!}Configuration value successfully updated.") - self.console.write("Setting %s to %s.." % (key, val)) + self.console.write("Setting '%s' to '%s'" % (key, val)) return client.core.set_config({key: val}).addCallback(on_set_config) def complete(self, text): diff --git a/deluge/ui/console/commands/connect.py b/deluge/ui/console/commands/connect.py index 5cd0bc5f4..b67ba31a2 100644 --- a/deluge/ui/console/commands/connect.py +++ b/deluge/ui/console/commands/connect.py @@ -8,27 +8,41 @@ # See LICENSE for more details. # +import logging + import deluge.component as component from deluge.ui.client import client from deluge.ui.console.main import BaseCommand +log = logging.getLogger(__name__) + class Command(BaseCommand): """Connect to a new deluge server.""" - usage = "Usage: connect " + usage = "Usage: connect [] []" - def handle(self, host="127.0.0.1:58846", username="", password="", **options): + def add_arguments(self, parser): + parser.add_argument("host", help="host and port", metavar="") + parser.add_argument("username", help="Username", metavar="", nargs="?", default="") + parser.add_argument("password", help="Password", metavar="", nargs="?", default="") + + def add_parser(self, subparsers): + parser = subparsers.add_parser(self.name, help=self.__doc__, description=self.__doc__, prog="connect") + self.add_arguments(parser) + + def handle(self, options): self.console = component.get("ConsoleUI") + + host = options.host try: host, port = host.split(":") + port = int(port) except ValueError: port = 58846 - else: - port = int(port) def do_connect(): - d = client.connect(host, port, username, password) + d = client.connect(host, port, options.username, options.password) def on_connect(result): if self.console.interactive: @@ -39,17 +53,18 @@ class Command(BaseCommand): try: msg = result.value.exception_msg except AttributeError: - msg = result.value.args[0] + msg = result.value.message self.console.write("{!error!}Failed to connect to %s:%s with reason: %s" % (host, port, msg)) return result - d.addCallback(on_connect) - d.addErrback(on_connect_fail) + d.addCallbacks(on_connect, on_connect_fail) return d if client.connected(): + def on_disconnect(result): - self.console.statusbars.update_statusbars() + if self.console.statusbars: + self.console.statusbars.update_statusbars() return do_connect() return client.disconnect().addCallback(on_disconnect) else: diff --git a/deluge/ui/console/commands/debug.py b/deluge/ui/console/commands/debug.py index 1a295e3a8..6ca11f42b 100644 --- a/deluge/ui/console/commands/debug.py +++ b/deluge/ui/console/commands/debug.py @@ -17,12 +17,14 @@ from deluge.ui.console.main import BaseCommand class Command(BaseCommand): """Enable and disable debugging""" - usage = "Usage: debug [on|off]" - def handle(self, state="", **options): - if state == "on": + def add_arguments(self, parser): + parser.add_argument("state", metavar="", choices=["on", "off"], help="The new state") + + def handle(self, options): + if options.state == "on": deluge.log.set_logger_level("debug") - elif state == "off": + elif options.state == "off": deluge.log.set_logger_level("error") else: component.get("ConsoleUI").write("{!error!}%s" % self.usage) diff --git a/deluge/ui/console/commands/gui.py b/deluge/ui/console/commands/gui.py index a37df3601..2d3ab8a2e 100644 --- a/deluge/ui/console/commands/gui.py +++ b/deluge/ui/console/commands/gui.py @@ -7,22 +7,26 @@ # See LICENSE for more details. # +import logging + import deluge.component as component from deluge.ui.console.main import BaseCommand from deluge.ui.console.modes.alltorrents import AllTorrents +log = logging.getLogger(__name__) + class Command(BaseCommand): - """Exit this mode and go into the more 'gui' like mode""" - usage = "Usage: gui" + """Enable interactive mode""" interactive_only = True - def handle(self, *args, **options): + def handle(self, options): console = component.get("ConsoleUI") try: at = component.get("AllTorrents") except KeyError: at = AllTorrents(console.stdscr, console.encoding) + console.set_mode(at) at._go_top = True at.resume() diff --git a/deluge/ui/console/commands/halt.py b/deluge/ui/console/commands/halt.py index 932142f8c..51ff14aa2 100644 --- a/deluge/ui/console/commands/halt.py +++ b/deluge/ui/console/commands/halt.py @@ -15,9 +15,8 @@ from deluge.ui.console.main import BaseCommand class Command(BaseCommand): "Shutdown the deluge server" - usage = "Usage: halt" - def handle(self, *args, **options): + def handle(self, options): self.console = component.get("ConsoleUI") def on_shutdown(result): diff --git a/deluge/ui/console/commands/help.py b/deluge/ui/console/commands/help.py index e922a3a55..c5f56b2bf 100644 --- a/deluge/ui/console/commands/help.py +++ b/deluge/ui/console/commands/help.py @@ -8,27 +8,32 @@ # See LICENSE for more details. # +import logging + from twisted.internet import defer import deluge.component as component from deluge.ui.console.main import BaseCommand +log = logging.getLogger(__name__) + class Command(BaseCommand): """displays help on other commands""" - usage = "Usage: help [command]" + def add_arguments(self, parser): + parser.add_argument("commands", metavar="", nargs="*", help="One or more commands") - def handle(self, *args, **options): + def handle(self, options): self.console = component.get("ConsoleUI") self._commands = self.console._commands deferred = defer.succeed(True) - if args: - for arg in args: + if options.commands: + for arg in options.commands: try: cmd = self._commands[arg] except KeyError: - self.console.write("{!error!}Unknown command %r" % args[0]) + self.console.write("{!error!}Unknown command %s" % arg) continue try: parser = cmd.create_parser() @@ -38,8 +43,15 @@ class Command(BaseCommand): self.console.write(" ") else: self.console.set_batch_write(True) + cmds_doc = "" for cmd in sorted(self._commands): - self.console.write("{!info!}" + cmd + "{!input!} - " + self._commands[cmd].__doc__ or '') + if cmd in self._commands[cmd].aliases: + continue + parser = self._commands[cmd].create_parser() + cmd_doc = "{!info!}" + "%-9s" % self._commands[cmd].name_with_alias + "{!input!} - "\ + + self._commands[cmd].__doc__ + "\n " + parser.format_usage() or '' + cmds_doc += parser.formatter.format_colors(cmd_doc) + self.console.write(cmds_doc) self.console.write(" ") self.console.write("For help on a specific command, use ' --help'") self.console.set_batch_write(False) diff --git a/deluge/ui/console/commands/info.py b/deluge/ui/console/commands/info.py index c10efdae4..c03331469 100644 --- a/deluge/ui/console/commands/info.py +++ b/deluge/ui/console/commands/info.py @@ -8,7 +8,6 @@ # See LICENSE for more details. # -from optparse import make_option from os.path import sep as dirsep import deluge.common as common @@ -90,44 +89,51 @@ class Command(BaseCommand): sort_help = "sort items. Possible keys: " + ", ".join(STATUS_KEYS) - option_list = BaseCommand.option_list + ( - make_option("-v", "--verbose", action="store_true", default=False, dest="verbose", - help="Show more information per torrent."), - make_option("-d", "--detailed", action="store_true", default=False, dest="detailed", - help="Show more detailed information including files and peers."), - make_option("-s", "--state", action="store", dest="state", - help="Show torrents with state STATE: %s." % (", ".join(STATES))), - make_option("--sort", action="store", type="string", default="", dest="sort", help=sort_help), - make_option("--sort-reverse", action="store", type="string", default="", dest="sort_rev", - help="Same as --sort but items are in reverse order.") - ) + epilog = """ + You can give the first few characters of a torrent-id to identify the torrent. - usage = """Usage: info [-v | -d | -s ] [ [ ...]] - You can give the first few characters of a torrent-id to identify the torrent. - info * will list all torrents. + Tab Completion (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. + | (To modify behaviour of third , set `third_tab_lists_all` to False) +""" - Tab Completion (info *pattern*): - | 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. - | (To modify behaviour of third , set `third_tab_lists_all` to False)""" + def add_arguments(self, parser): + parser.add_argument("-v", "--verbose", action="store_true", default=False, dest="verbose", + help="Show more information per torrent.") + parser.add_argument("-d", "--detailed", action="store_true", default=False, dest="detailed", + help="Show more detailed information including files and peers.") + parser.add_argument("-s", "--state", action="store", dest="state", + help="Show torrents with state STATE: %s." % (", ".join(STATES))) + parser.add_argument("--sort", action="store", type=str, default="", dest="sort", help=self.sort_help) + parser.add_argument("--sort-reverse", action="store", type=str, default="", dest="sort_rev", + help="Same as --sort but items are in reverse order.") + parser.add_argument("torrent_ids", metavar="", nargs="*", + help="One or more torrent ids. If none is given, list all") - def handle(self, *args, **options): + def add_subparser(self, subparsers): + parser = subparsers.add_parser(self.name, prog=self.name, help=self.__doc__, + description=self.__doc__, epilog=self.epilog) + self.add_arguments(parser) + + def handle(self, options): self.console = component.get("ConsoleUI") # Compile a list of torrent_ids to request the status of torrent_ids = [] - for arg in args: - torrent_ids.extend(self.console.match_torrent(arg)) - if not args: + if options.torrent_ids: + for t_id in options.torrent_ids: + torrent_ids.extend(self.console.match_torrent(t_id)) + else: torrent_ids.extend(self.console.match_torrent("")) def on_torrents_status(status): # Print out the information for each torrent - sort_key = options["sort"] + sort_key = options.sort sort_reverse = False if not sort_key: - sort_key = options["sort_rev"] + sort_key = options.sort_rev sort_reverse = True if not sort_key: sort_key = "name" @@ -138,19 +144,19 @@ class Command(BaseCommand): sort_key = "name" sort_reverse = False for key, value in sorted(status.items(), key=lambda x: x[1].get(sort_key), reverse=sort_reverse): - self.show_info(key, status[key], options["verbose"], options["detailed"]) + self.show_info(key, status[key], options.verbose, options.detailed) def on_torrents_status_fail(reason): self.console.write("{!error!}Error getting torrent info: %s" % reason) status_dict = {"id": torrent_ids} - if options["state"]: - options["state"] = options["state"].capitalize() - if options["state"] in STATES: - status_dict["state"] = options["state"] + if options.state: + options.state = options.state.capitalize() + if options.state in STATES: + status_dict.state = options.state else: - self.console.write("Invalid state: %s" % options["state"]) + self.console.write("Invalid state: %s" % options.state) self.console.write("Possible values are: %s." % (", ".join(STATES))) return diff --git a/deluge/ui/console/commands/manage.py b/deluge/ui/console/commands/manage.py index baa9569fc..59a2edc3f 100644 --- a/deluge/ui/console/commands/manage.py +++ b/deluge/ui/console/commands/manage.py @@ -8,8 +8,6 @@ # See LICENSE for more details. # -from optparse import make_option - from twisted.internet import defer import deluge.component as component @@ -35,20 +33,25 @@ torrent_options = { class Command(BaseCommand): """Show and manage per-torrent options""" - option_list = BaseCommand.option_list + ( - make_option("-s", "--set", action="store", nargs=2, dest="set", help="set value for key"), - ) - usage = "Usage: manage [ [ ...]]\n"\ - " manage --set " + usage = "manage [--set ] [ [...] ]" - def handle(self, *args, **options): + def add_arguments(self, parser): + parser.add_argument("torrent", metavar="", + help="an expression matched against torrent ids and torrent names") + set_group = parser.add_argument_group("setting a value") + set_group.add_argument("-s", "--set", action="store", metavar="", help="set value for this key") + set_group.add_argument("values", metavar="", nargs="+", help="Value to set") + get_group = parser.add_argument_group("getting values") + get_group.add_argument("keys", metavar="", nargs="*", help="one or more keys separated by space") + + def handle(self, options): self.console = component.get("ConsoleUI") - if options['set']: - return self._set_option(*args, **options) + if options.set: + return self._set_option(options) else: - return self._get_option(*args, **options) + return self._get_option(options) - def _get_option(self, *args, **options): + def _get_option(self, options): def on_torrents_status(status): for torrentid, data in status.items(): @@ -63,11 +66,10 @@ class Command(BaseCommand): def on_torrents_status_fail(reason): self.console.write('{!error!}Failed to get torrent data.') - torrent_ids = [] - torrent_ids.extend(self.console.match_torrent(args[0])) + torrent_ids = self.console.match_torrent(options.torrent) request_options = [] - for opt in args[1:]: + for opt in options.values: if opt not in torrent_options: self.console.write('{!error!}Unknown torrent option: %s' % opt) return @@ -77,16 +79,14 @@ class Command(BaseCommand): request_options.append('name') d = client.core.get_torrents_status({"id": torrent_ids}, request_options) - d.addCallback(on_torrents_status) - d.addErrback(on_torrents_status_fail) + d.addCallbacks(on_torrents_status, on_torrents_status_fail) return d - def _set_option(self, *args, **options): + def _set_option(self, options): deferred = defer.Deferred() - torrent_ids = [] - torrent_ids.extend(self.console.match_torrent(args[0])) - key = options["set"][0] - val = options["set"][1] + " " .join(args[1:]) + key = options.set + val = " " .join(options.values) + torrent_ids = self.console.match_torrent(options.torrent) if key not in torrent_options: self.console.write("{!error!}The key '%s' is invalid!" % key) diff --git a/deluge/ui/console/commands/move.py b/deluge/ui/console/commands/move.py index 83e5794c2..21d6174b9 100644 --- a/deluge/ui/console/commands/move.py +++ b/deluge/ui/console/commands/move.py @@ -7,43 +7,43 @@ # See LICENSE for more details. # +import logging import os.path import deluge.component as component from deluge.ui.client import client from deluge.ui.console.main import BaseCommand +log = logging.getLogger(__name__) + class Command(BaseCommand): """Move torrents' storage location""" - usage = "Usage: move [ ...] " - def handle(self, *args, **options): + def add_arguments(self, parser): + parser.add_argument("torrent_ids", metavar="", nargs="+", help="One or more torrent ids") + parser.add_argument("path", metavar="", help="The path to move the torrents to") + + def handle(self, options): self.console = component.get("ConsoleUI") - if len(args) < 2: - self.console.write(self.usage) - return - - path = args[-1] - - if os.path.exists(path) and not os.path.isdir(path): - self.console.write("{!error!}Cannot Move Download Folder: %s exists and is not a directory" % path) + if os.path.exists(options.path) and not os.path.isdir(options.path): + self.console.write("{!error!}Cannot Move Download Folder: %s exists and is not a directory" % options.path) return ids = [] - for i in args[:-1]: - ids.extend(self.console.match_torrent(i)) - names = [] - for i in ids: - names.append(self.console.get_torrent_name(i)) - namestr = ", ".join(names) + for t_id in options.torrent_ids: + tid = self.console.match_torrent(t_id) + ids.extend(tid) + names.append(self.console.get_torrent_name(tid)) def on_move(res): - self.console.write("Moved \"%s\" to %s" % (namestr, path)) + msg = "Moved \"%s\" to %s" % (", ".join(names), options.path) + self.console.write(msg) + log.info(msg) - d = client.core.move_storage(ids, path) + d = client.core.move_storage(ids, options.path) d.addCallback(on_move) return d @@ -81,5 +81,4 @@ class Command(BaseCommand): if os.path.isdir(p): p += "/" ret.append(p) - return ret diff --git a/deluge/ui/console/commands/pause.py b/deluge/ui/console/commands/pause.py index b670c4a1c..13d5affd8 100644 --- a/deluge/ui/console/commands/pause.py +++ b/deluge/ui/console/commands/pause.py @@ -14,21 +14,22 @@ from deluge.ui.console.main import BaseCommand class Command(BaseCommand): - """Pause a torrent""" - usage = "Usage: pause [ * | [ ...] ]" + """Pause torrents""" + usage = "pause [ * | [ ...] ]" - def handle(self, *args, **options): + def add_arguments(self, parser): + parser.add_argument("torrent_ids", metavar="", nargs="+", + help="One or more torrent ids. '*' pauses all torrents") + + def handle(self, options): self.console = component.get("ConsoleUI") - if len(args) == 0: - self.console.write(self.usage) - return - if len(args) > 0 and args[0].lower() == '*': + if options.torrent_ids[0] == "*": client.core.pause_session() return torrent_ids = [] - for arg in args: + for arg in options.torrent_ids: torrent_ids.extend(self.console.match_torrent(arg)) if torrent_ids: diff --git a/deluge/ui/console/commands/plugin.py b/deluge/ui/console/commands/plugin.py index 066dda1ac..58d9feb6e 100644 --- a/deluge/ui/console/commands/plugin.py +++ b/deluge/ui/console/commands/plugin.py @@ -7,8 +7,6 @@ # See LICENSE for more details. # -from optparse import make_option - import deluge.component as component import deluge.configmanager from deluge.ui.client import client @@ -16,41 +14,28 @@ from deluge.ui.console.main import BaseCommand class Command(BaseCommand): - """Manage plugins with this command""" - option_list = BaseCommand.option_list + ( - make_option("-l", "--list", action="store_true", default=False, dest="list", - help="Lists available plugins"), - make_option("-s", "--show", action="store_true", default=False, dest="show", - help="Shows enabled plugins"), - make_option("-e", "--enable", dest="enable", - help="Enables a plugin"), - make_option("-d", "--disable", dest="disable", - help="Disables a plugin"), - make_option("-r", "--reload", action="store_true", default=False, dest="reload", - help="Reload list of available plugins"), - make_option("-i", "--install", dest="plugin_file", - help="Install a plugin from an .egg file"), - ) - usage = """Usage: plugin [ -l | --list ] - plugin [ -s | --show ] - plugin [ -e | --enable ] - plugin [ -d | --disable ] - plugin [ -i | --install ] - plugin [ -r | --reload]""" + """Manage plugins""" - def handle(self, *args, **options): + def add_arguments(self, parser): + parser.add_argument("-l", "--list", action="store_true", default=False, dest="list", + help="Lists available plugins") + parser.add_argument("-s", "--show", action="store_true", default=False, dest="show", + help="Shows enabled plugins") + parser.add_argument("-e", "--enable", dest="enable", nargs="+", help="Enables a plugin") + parser.add_argument("-d", "--disable", dest="disable", nargs="+", help="Disables a plugin") + parser.add_argument("-r", "--reload", action="store_true", default=False, dest="reload", + help="Reload list of available plugins") + parser.add_argument("-i", "--install", help="Install a plugin from an .egg file") + + def handle(self, options): self.console = component.get("ConsoleUI") - if len(args) == 0 and not any(options.values()): - self.console.write(self.usage) - return - - if options["reload"]: + if options.reload: client.core.pluginmanager.rescan_plugins() self.console.write("{!green!}Plugin list successfully reloaded") return - elif options["list"]: + elif options.list: def on_available_plugins(result): self.console.write("{!info!}Available Plugins:") for p in result: @@ -58,7 +43,7 @@ class Command(BaseCommand): return client.core.get_available_plugins().addCallback(on_available_plugins) - elif options["show"]: + elif options.show: def on_enabled_plugins(result): self.console.write("{!info!}Enabled Plugins:") for p in result: @@ -66,50 +51,42 @@ class Command(BaseCommand): return client.core.get_enabled_plugins().addCallback(on_enabled_plugins) - elif options["enable"]: + elif options.enable: def on_available_plugins(result): plugins = {} for p in result: plugins[p.lower()] = p - p_args = [options["enable"]] + list(args) - - for arg in p_args: + for arg in options.enable: if arg.lower() in plugins: client.core.enable_plugin(plugins[arg.lower()]) return client.core.get_available_plugins().addCallback(on_available_plugins) - elif options["disable"]: + elif options.disable: def on_enabled_plugins(result): plugins = {} for p in result: plugins[p.lower()] = p - p_args = [options["disable"]] + list(args) - - for arg in p_args: + for arg in options.disable: if arg.lower() in plugins: client.core.disable_plugin(plugins[arg.lower()]) return client.core.get_enabled_plugins().addCallback(on_enabled_plugins) - elif options["plugin_file"]: - - filepath = options["plugin_file"] - + elif options.install: import os.path import base64 import shutil + filepath = options.install + if not os.path.exists(filepath): self.console.write("{!error!}Invalid path: %s" % filepath) return config_dir = deluge.configmanager.get_config_dir() filename = os.path.split(filepath)[1] - - shutil.copyfile( - filepath, - os.path.join(config_dir, "plugins", filename)) + shutil.copyfile(filepath, os.path.join(config_dir, "plugins", filename)) client.core.rescan_plugins() diff --git a/deluge/ui/console/commands/quit.py b/deluge/ui/console/commands/quit.py index 0272f87f2..7c9be5f47 100644 --- a/deluge/ui/console/commands/quit.py +++ b/deluge/ui/console/commands/quit.py @@ -15,11 +15,11 @@ from deluge.ui.console.main import BaseCommand class Command(BaseCommand): - """Exit from the client.""" + """Exit the client.""" aliases = ["exit"] interactive_only = True - def handle(self, *args, **options): + def handle(self, options): if client.connected(): def on_disconnect(result): reactor.stop() diff --git a/deluge/ui/console/commands/recheck.py b/deluge/ui/console/commands/recheck.py index d0f4e0202..ed4b1d9f4 100644 --- a/deluge/ui/console/commands/recheck.py +++ b/deluge/ui/console/commands/recheck.py @@ -14,20 +14,20 @@ from deluge.ui.console.main import BaseCommand class Command(BaseCommand): """Forces a recheck of the torrent data""" - usage = "Usage: recheck [ * | [ ...] ]" + usage = "recheck [ * | [ ...] ]" - def handle(self, *args, **options): + def add_arguments(self, parser): + parser.add_argument("torrent_ids", metavar="", nargs="+", help="One or more torrent ids") + + def handle(self, options): self.console = component.get("ConsoleUI") - if len(args) == 0: - self.console.write(self.usage) - return - if len(args) > 0 and args[0].lower() == "*": + if options.torrent_ids[0] == "*": client.core.force_recheck(self.console.match_torrent("")) return torrent_ids = [] - for arg in args: + for arg in options.torrent_ids: torrent_ids.extend(self.console.match_torrent(arg)) if torrent_ids: diff --git a/deluge/ui/console/commands/resume.py b/deluge/ui/console/commands/resume.py index 9e61b3bcc..a72be6063 100644 --- a/deluge/ui/console/commands/resume.py +++ b/deluge/ui/console/commands/resume.py @@ -14,22 +14,23 @@ from deluge.ui.console.main import BaseCommand class Command(BaseCommand): - """Resume a torrent""" - usage = "Usage: resume [ * | [ ...] ]" + """Resume torrents""" + usage = "resume [ * | [ ...] ]" - def handle(self, *args, **options): + def add_arguments(self, parser): + parser.add_argument("torrent_ids", metavar="", nargs="+", + help="One or more torrent ids. '*' resumes all torrents") + + def handle(self, options): self.console = component.get("ConsoleUI") - if len(args) == 0: - self.console.write(self.usage) - return - if len(args) > 0 and args[0] == "*": + if options.torrent_ids[0] == "*": client.core.resume_session() return torrent_ids = [] - for arg in args: - torrent_ids.extend(self.console.match_torrent(arg)) + for t_id in options.torrent_ids: + torrent_ids.extend(self.console.match_torrent(t_id)) if torrent_ids: return client.core.resume_torrent(torrent_ids) diff --git a/deluge/ui/console/commands/rm.py b/deluge/ui/console/commands/rm.py index 82e8331c6..324c0132d 100644 --- a/deluge/ui/console/commands/rm.py +++ b/deluge/ui/console/commands/rm.py @@ -8,8 +8,6 @@ # See LICENSE for more details. # -from optparse import make_option - import deluge.component as component from deluge.ui.client import client from deluge.ui.console.main import BaseCommand @@ -17,21 +15,16 @@ from deluge.ui.console.main import BaseCommand class Command(BaseCommand): """Remove a torrent""" - usage = "Usage: rm " aliases = ["del"] - option_list = BaseCommand.option_list + ( - make_option("--remove_data", action="store_true", default=False, - help="remove the torrent's data"), - ) + def add_arguments(self, parser): + parser.add_argument("--remove_data", action="store_true", default=False, help="remove the torrent's data") + parser.add_argument("torrent_ids", metavar="", nargs="+", help="One or more torrent ids") - def handle(self, *args, **options): + def handle(self, options): self.console = component.get("ConsoleUI") - if len(args) == 0: - self.console.write(self.usage) - torrent_ids = [] - for arg in args: + for arg in options.torrent_ids: torrent_ids.extend(self.console.match_torrent(arg)) def on_removed_finished(errors): @@ -40,7 +33,7 @@ class Command(BaseCommand): for t_id, e_msg in errors: self.console.write("Error removing torrent %s : %s" % (t_id, e_msg)) - d = client.core.remove_torrents(torrent_ids, options["remove_data"]) + d = client.core.remove_torrents(torrent_ids, options.remove_data) d.addCallback(on_removed_finished) def complete(self, line): diff --git a/deluge/ui/console/commands/status.py b/deluge/ui/console/commands/status.py index 5077a7231..704d1f267 100644 --- a/deluge/ui/console/commands/status.py +++ b/deluge/ui/console/commands/status.py @@ -7,7 +7,7 @@ # See LICENSE for more details. # -from optparse import make_option +import logging from twisted.internet import defer @@ -16,44 +16,34 @@ from deluge.common import TORRENT_STATE, fspeed from deluge.ui.client import client from deluge.ui.console.main import BaseCommand +log = logging.getLogger(__name__) + class Command(BaseCommand): """Shows a various status information from the daemon.""" - option_list = BaseCommand.option_list + ( - make_option("-r", "--raw", action="store_true", default=False, dest="raw", - help="Don't format upload/download rates in KiB/s \ -(useful for scripts that want to do their own parsing)"), - make_option("-n", "--no-torrents", action="store_false", default=True, dest="show_torrents", - help="Don't show torrent status (this will make the command a bit faster)"), - ) - usage = "Usage: status [-r] [-n]" + def add_arguments(self, parser): + parser.add_argument("-r", "--raw", action="store_true", default=False, dest="raw", + help="Don't format upload/download rates in KiB/s \ + (useful for scripts that want to do their own parsing)") + parser.add_argument("-n", "--no-torrents", action="store_false", default=True, dest="show_torrents", + help="Don't show torrent status (this will make the command a bit faster)") - def handle(self, *args, **options): + def handle(self, options): self.console = component.get("ConsoleUI") self.status = None - self.connections = None - if options["show_torrents"]: - self.torrents = None - else: - self.torrents = -2 - - self.raw = options["raw"] + self.torrents = 1 if options.show_torrents else 0 + self.raw = options.raw def on_session_status(status): self.status = status - if self.status is not None and self.connections is not None and self.torrents is not None: - self.print_status() def on_torrents_status(status): self.torrents = status - if self.status is not None and self.connections is not None and self.torrents is not None: - self.print_status() def on_torrents_status_fail(reason): - self.torrents = -1 - if self.status is not None and self.connections is not None and self.torrents is not None: - self.print_status() + log.warn("Failed to retrieve session status: %s", reason) + self.torrents = -2 deferreds = [] @@ -61,15 +51,15 @@ class Command(BaseCommand): ds.addCallback(on_session_status) deferreds.append(ds) - if options["show_torrents"]: + if options.show_torrents: dt = client.core.get_torrents_status({}, ["state"]) dt.addCallback(on_torrents_status) dt.addErrback(on_torrents_status_fail) deferreds.append(dt) - return defer.DeferredList(deferreds) + return defer.DeferredList(deferreds).addCallback(self.print_status) - def print_status(self): + def print_status(self, *args): self.console.set_batch_write(True) if self.raw: self.console.write("{!info!}Total upload: %f" % self.status["payload_upload_rate"]) @@ -78,12 +68,12 @@ class Command(BaseCommand): self.console.write("{!info!}Total upload: %s" % fspeed(self.status["payload_upload_rate"])) self.console.write("{!info!}Total download: %s" % fspeed(self.status["payload_download_rate"])) self.console.write("{!info!}DHT Nodes: %i" % self.status["dht_nodes"]) - self.console.write("{!info!}Total connections: %i" % self.connections) - if self.torrents == -1: - self.console.write("{!error!}Error getting torrent info") - elif self.torrents != -2: - self.console.write("{!info!}Total torrents: %i" % len(self.torrents)) + if isinstance(self.torrents, int): + if self.torrents == -2: + self.console.write("{!error!}Error getting torrent info") + else: + self.console.write("{!info!}Total torrents: %i" % len(self.torrents)) state_counts = {} for state in TORRENT_STATE: state_counts[state] = 0 diff --git a/deluge/ui/console/commands/update_tracker.py b/deluge/ui/console/commands/update_tracker.py index 8e80b83a2..296f8cae1 100644 --- a/deluge/ui/console/commands/update_tracker.py +++ b/deluge/ui/console/commands/update_tracker.py @@ -15,15 +15,17 @@ from deluge.ui.console.main import BaseCommand class Command(BaseCommand): """Update tracker for torrent(s)""" - usage = "Usage: update_tracker [ * | [ ...] ]" + usage = "update_tracker [ * | [ ...] ]" aliases = ['reannounce'] - def handle(self, *args, **options): + def add_arguments(self, parser): + parser.add_argument('torrent_ids', metavar="", nargs='+', + help='One or more torrent ids. "*" updates all torrents') + + def handle(self, options): self.console = component.get("ConsoleUI") - if len(args) == 0: - self.console.write(self.usage) - return - if len(args) > 0 and args[0].lower() == '*': + args = options.torrent_ids + if options.torrent_ids[0] == "*": args = [""] torrent_ids = [] diff --git a/deluge/ui/console/console.py b/deluge/ui/console/console.py index 096b46454..a27298e6d 100644 --- a/deluge/ui/console/console.py +++ b/deluge/ui/console/console.py @@ -8,29 +8,36 @@ # See LICENSE for more details. # -import optparse import os -import sys +from deluge.ui.baseargparser import DelugeTextHelpFormatter from deluge.ui.console import UI_PATH from deluge.ui.ui import UI -def load_commands(command_dir, exclude=[]): +# +# Note: Cannot import from console.main here because it imports the twisted reactor. +# Console is imported from console/__init__.py loaded by the script entry points +# defined in setup.py +# + +def load_commands(command_dir): + 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('_'): + if 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: + cmd._name = filename.split('.')[len(filename.split('.')) - 2] + names = [cmd._name] + names.extend(cmd.aliases) + for a in names: commands.append((a, cmd)) return dict(commands) except OSError: @@ -44,61 +51,29 @@ class Console(UI): 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) + group = self.parser.add_argument_group(_("Console Options"), "These daemon connect options will be " + "used for commands, or if console ui autoconnect is enabled.") + group.add_argument("-d", "--daemon", dest="daemon_addr", required=False, default="127.0.0.1") + group.add_argument("-p", "--port", dest="daemon_port", type=int, required=False, default="58846") + group.add_argument("-U", "--username", dest="daemon_user", required=False) + group.add_argument("-P", "--password", dest="daemon_pass", required=False) - 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) + # 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, + 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", + 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): - 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)) + from deluge.ui.console.main import ConsoleUI # import here because (see top) + ConsoleUI(self.options, self.console_cmds) diff --git a/deluge/ui/console/main.py b/deluge/ui/console/main.py index e75731a9d..128b709be 100644 --- a/deluge/ui/console/main.py +++ b/deluge/ui/console/main.py @@ -10,10 +10,9 @@ from __future__ import print_function +import argparse import locale import logging -import optparse -import re import shlex import sys @@ -21,8 +20,10 @@ from twisted.internet import defer, reactor import deluge.common import deluge.component as component +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.eventlog import EventLog from deluge.ui.console.statusbars import StatusBars from deluge.ui.coreconfig import CoreConfig @@ -31,84 +32,32 @@ from deluge.ui.sessionproxy import SessionProxy log = logging.getLogger(__name__) -class DelugeHelpFormatter(optparse.IndentedHelpFormatter): - """ - Format help in a way suited to deluge Legacy mode - colors, format, indentation... - """ +class ConsoleCommandParser(argparse.ArgumentParser): - replace_dict = { - "": "{!green!}%s{!input!}", - "": "{!yellow!}%s{!input!}", - "\\.\\.\\.": "{!yellow!}%s{!input!}", - "\\s\\*\\s": "{!blue!}%s{!input!}", - "(?": "{!white!}%s{!input!}", - "[_A-Z]{3,}": "{!cyan!}%s{!input!}", - - "": "{!yellow!}%s{!input!}", - "": "{!green!}%s{!input!}" - - } - - def __init__(self, - indent_increment=2, - max_help_position=24, - width=None, - short_first=1): - optparse.IndentedHelpFormatter.__init__( - self, indent_increment, max_help_position, width, short_first) - - def _format_colors(self, string): - def r(repl): - return lambda s: repl % s.group() - - for key, replacement in self.replace_dict.items(): - string = re.sub(key, r(replacement), string) - - return string - - def format_usage(self, usage): - - return _("{!info!}Usage{!input!}: %s\n") % self._format_colors(usage) - - def format_option(self, option): - result = [] - opts = self.option_strings[option] - opt_width = self.help_position - self.current_indent - 2 - if len(opts) > opt_width: - opts = "%*s%s\n" % (self.current_indent, "", opts) - opts = self._format_colors(opts) - indent_first = self.help_position - else: # start help on same line as opts - opts = "%*s%-*s " % (self.current_indent, "", opt_width, opts) - opts = self._format_colors(opts) - indent_first = 0 - result.append(opts) - if option.help: - help_text = self.expand_default(option) - help_text = self._format_colors(help_text) - help_lines = optparse.textwrap.wrap(help_text, self.help_width) - result.append("%*s%s\n" % (indent_first, "", help_lines[0])) - result.extend(["%*s%s\n" % (self.help_position, "", line) - for line in help_lines[1:]]) - elif opts[-1] != "\n": - result.append("\n") - return "".join(result) + """ + # Handle epilog manually to keep the text formatting + epilog = self.epilog + self.epilog = "" + help_str = super(ConsoleCommandParser, self).format_help() + if epilog is not None: + help_str += epilog + self.epilog = epilog + return help_str -class OptionParser(optparse.OptionParser): - """subclass from optparse.OptionParser so exit() won't exit.""" +class OptionParser(ConsoleCommandParser): + def __init__(self, **kwargs): - optparse.OptionParser.__init__(self, **kwargs) - - self.formatter = DelugeHelpFormatter() + super(OptionParser, self).__init__(**kwargs) + self.formatter = ConsoleColorFormatter() def exit(self, status=0, msg=None): - self.values._exit = True + self._exit = True if msg: print(msg) @@ -124,7 +73,7 @@ class OptionParser(optparse.OptionParser): def print_usage(self, _file=None): console = component.get("ConsoleUI") if self.usage: - for line in self.get_usage().splitlines(): + for line in self.format_usage().splitlines(): console.write(line) def print_help(self, _file=None): @@ -134,43 +83,36 @@ class OptionParser(optparse.OptionParser): console.write(line) console.set_batch_write(False) - def format_option_help(self, formatter=None): - if formatter is None: - formatter = self.formatter - formatter.store_option_strings(self) - result = [] - result.append(formatter.format_heading(_("{!info!}Options{!input!}"))) - formatter.indent() - if self.option_list: - result.append(optparse.OptionContainer.format_option_help(self, formatter)) - result.append("\\n") - for group in self.option_groups: - result.append(group.format_help(formatter)) - result.append("\\n") - formatter.dedent() - # Drop the last "\\n", or the header if no options or option groups: - return "".join(result[:-1]) + def format_help(self): + """Return help formatted with colors.""" + help_str = super(OptionParser, self).format_help() + return self.formatter.format_colors(help_str) class BaseCommand(object): - usage = "usage" + usage = None interactive_only = False - option_list = tuple() aliases = [] + _name = "base" + epilog = "" def complete(self, text, *args): return [] - def handle(self, *args, **options): + def handle(self, options): pass @property def name(self): - return "base" + return self._name @property - def epilog(self): + def name_with_alias(self): + return "/".join([self._name] + self.aliases) + + @property + def description(self): return self.__doc__ def split(self, text): @@ -183,16 +125,32 @@ class BaseCommand(object): return result def create_parser(self): - return OptionParser(prog=self.name, usage=self.usage, epilog=self.epilog, option_list=self.option_list) + opts = {"prog": self.name_with_alias, "description": self.__doc__, "epilog": self.epilog} + if self.usage: + opts["usage"] = self.usage + parser = OptionParser(**opts) + parser.add_argument(self.name, metavar="") + self.add_arguments(parser) + return parser + + def add_subparser(self, subparsers): + opts = {"prog": self.name_with_alias, "help": self.__doc__, "description": self.__doc__} + if self.usage: + opts["usage"] = self.usage + parser = subparsers.add_parser(self.name, **opts) + self.add_arguments(parser) + + def add_arguments(self, parser): + pass class ConsoleUI(component.Component): - def __init__(self, args=None, cmds=None, daemon=None): - component.Component.__init__(self, "ConsoleUI", 2) + def __init__(self, options=None, cmds=None): + component.Component.__init__(self, "ConsoleUI", 2) # keep track of events for the log view self.events = [] - + self.statusbars = None try: locale.setlocale(locale.LC_ALL, "") self.encoding = locale.getpreferredencoding() @@ -209,19 +167,14 @@ class ConsoleUI(component.Component): # Set the interactive flag to indicate where we should print the output self.interactive = True self._commands = cmds - if args: - args = " ".join(args) + + if options.remaining: self.interactive = False if not cmds: print("Sorry, couldn't find any commands") return else: - from deluge.ui.console.commander import Commander - cmdr = Commander(cmds) - if daemon: - cmdr.exec_args(args, *daemon) - else: - cmdr.exec_args(args, None, None, None, None) + self.exec_args(options) self.coreconfig = CoreConfig() if self.interactive and not deluge.common.windows_check(): @@ -240,6 +193,60 @@ Please use commands from the command line, eg:\n else: 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): + def on_started(result): + def on_started(result): + def do_command(result, cmd): + return commander.do_command(cmd) + d = defer.succeed(None) + for command in commands: + if command in ("quit", "exit"): + break + d.addCallback(do_command, command) + d.addCallback(do_command, "quit") + + # We need to wait for the rpcs in start() to finish before processing + # any of the commands. + self.started_deferred.addCallback(on_started) + component.start().addCallback(on_started) + + def on_connect_fail(reason): + if reason.check(DelugeError): + rm = reason.getErrorMessage() + else: + rm = reason.value.message + print("Could not connect to daemon: %s:%s\n %s" % (options.daemon_addr, options.daemon_port, rm)) + 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: + 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) + d.addCallback(on_connect) + d.addErrback(on_connect_fail) + def run(self, stdscr): """ This method is called by the curses.wrapper to start the mainloop and @@ -332,6 +339,7 @@ Please use commands from the command line, eg:\n self.screen = mode self.statusbars.screen = self.screen reactor.addReader(self.screen) + self.stdscr.clear() mode.refresh() def on_client_disconnect(self): diff --git a/deluge/ui/console/modes/alltorrents.py b/deluge/ui/console/modes/alltorrents.py index 8bbc1c86c..40453a3ca 100644 --- a/deluge/ui/console/modes/alltorrents.py +++ b/deluge/ui/console/modes/alltorrents.py @@ -812,9 +812,6 @@ class AllTorrents(BaseMode, component.Component): self.refresh() def refresh(self, lines=None): - # log.error("ref") - # import traceback - # traceback.print_stack() # Something has requested we scroll to the top of the list if self._go_top: self.cursel = 1 diff --git a/deluge/ui/console/modes/connectionmanager.py b/deluge/ui/console/modes/connectionmanager.py index c86c2ae66..f3c5ab8f0 100644 --- a/deluge/ui/console/modes/connectionmanager.py +++ b/deluge/ui/console/modes/connectionmanager.py @@ -38,6 +38,7 @@ DEFAULT_CONFIG = { class ConnectionManager(BaseMode): + def __init__(self, stdscr, encoding=None): self.popup = None self.statuses = {} @@ -87,6 +88,7 @@ class ConnectionManager(BaseMode): port = host[2] user = host[3] password = host[4] + log.debug("connect: hadr=%s, port=%s, user=%s, password=%s", hadr, port, user, password) d = c.connect(hadr, port, user, password) d.addCallback(on_connect, c, host[0]) d.addErrback(on_connect_failed, host[0]) diff --git a/deluge/ui/console/modes/legacy.py b/deluge/ui/console/modes/legacy.py index afe95328a..46b71cae8 100644 --- a/deluge/ui/console/modes/legacy.py +++ b/deluge/ui/console/modes/legacy.py @@ -24,6 +24,7 @@ import deluge.component as component import deluge.configmanager import deluge.ui.console.colors as colors from deluge.ui.client import client +from deluge.ui.console.commander import Commander from deluge.ui.console.modes import format_utils from deluge.ui.console.modes.basemode import BaseMode @@ -94,11 +95,15 @@ def commonprefix(m): return s2 -class Legacy(BaseMode, component.Component): +class Legacy(BaseMode, Commander, component.Component): def __init__(self, stdscr, encoding=None): component.Component.__init__(self, "LegacyUI", 1, depend=["SessionProxy"]) + # Get a handle to the main console + self.console = component.get("ConsoleUI") + Commander.__init__(self, self.console._commands, interactive=True) + self.batch_write = False # A list of strings to be displayed based on the offset (scroll) @@ -122,9 +127,6 @@ class Legacy(BaseMode, component.Component): # Keep track of double- and multi-tabs self.tab_count = 0 - # Get a handle to the main console - self.console = component.get("ConsoleUI") - self.console_config = component.get("AllTorrents").config # To avoid having to truncate the file every time we're writing @@ -586,64 +588,6 @@ class Legacy(BaseMode, component.Component): col += strwidth(s) - def do_command(self, cmd): - """ - Processes a command. - - :param cmd: str, the command string - - """ - if not cmd: - return - cmd, _, line = cmd.partition(" ") - try: - parser = self.console._commands[cmd].create_parser() - except KeyError: - self.write("{!error!}Unknown command: %s" % cmd) - return - - try: - args = self.console._commands[cmd].split(line) - except ValueError as ex: - self.write("{!error!}Error parsing command: %s" % ex) - return - - # Do a little hack here to print 'command --help' properly - parser._print_help = parser.print_help - - def print_help(f=None): - parser._print_help(f) - parser.print_help = print_help - - # Only these commands can be run when not connected to a daemon - not_connected_cmds = ["help", "connect", "quit"] - aliases = [] - for c in not_connected_cmds: - aliases.extend(self.console._commands[c].aliases) - not_connected_cmds.extend(aliases) - - if not client.connected() and cmd not in not_connected_cmds: - self.write("{!error!}Not connected to a daemon, please use the connect command first.") - return - - try: - options, args = parser.parse_args(args) - except TypeError as ex: - self.write("{!error!}Error parsing options: %s" % ex) - return - - if not getattr(options, "_exit", False): - try: - ret = self.console._commands[cmd].handle(*args, **options.__dict__) - 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 - def set_batch_write(self, batch): """ When this is set the screen is not refreshed after a `:meth:write` until diff --git a/deluge/ui/gtkui/gtkui.py b/deluge/ui/gtkui/gtkui.py index a1b6f4040..21695e241 100644 --- a/deluge/ui/gtkui/gtkui.py +++ b/deluge/ui/gtkui/gtkui.py @@ -8,6 +8,7 @@ # # We skip isorting this file as it want to move the gtk2reactor.install() below the imports # isort:skip_file + from __future__ import division import logging @@ -24,7 +25,7 @@ from twisted.internet.task import LoopingCall try: # Install twisted reactor, before any other modules import reactor. reactor = gtk2reactor.install() -except ReactorAlreadyInstalledError: +except ReactorAlreadyInstalledError as ex: # Running unit tests so trial already installed a rector pass @@ -196,7 +197,7 @@ class GtkUI(object): # Start the IPC Interface before anything else.. Just in case we are # already running. self.queuedtorrents = QueuedTorrents() - self.ipcinterface = IPCInterface(args) + self.ipcinterface = IPCInterface(args.torrents) # Initialize gdk threading gtk.gdk.threads_init() diff --git a/deluge/ui/ui.py b/deluge/ui/ui.py index 2e53aefe3..1d9d06f7f 100644 --- a/deluge/ui/ui.py +++ b/deluge/ui/ui.py @@ -8,12 +8,13 @@ # import logging -import optparse import deluge.common import deluge.configmanager import deluge.log -from deluge.commonoptions import CommonOptionParser +from deluge.ui.baseargparser import BaseArgParser + +log = logging.getLogger(__name__) try: from setproctitle import setproctitle @@ -21,10 +22,6 @@ except ImportError: def setproctitle(title): return -DEFAULT_PREFS = { - "default_ui": "gtk" -} - if 'dev' not in deluge.common.get_version(): import warnings warnings.filterwarnings('ignore', category=DeprecationWarning, module='twisted') @@ -32,15 +29,16 @@ if 'dev' not in deluge.common.get_version(): class UI(object): - def __init__(self, name="gtk", skip_common=False): + def __init__(self, name="gtk", parser=None): self.__name = name + self.__parser = parser if parser else BaseArgParser() + deluge.common.setup_translations(setup_pygtk=(name == "gtk")) - if name == "gtk": - deluge.common.setup_translations(setup_pygtk=True) - else: - deluge.common.setup_translations() - - self.__parser = optparse.OptionParser() if skip_common else CommonOptionParser() + def parse_args(self, args=None): + options = self.parser.parse_args(args) + if not hasattr(options, "remaining"): + options.remaining = [] + return options @property def name(self): @@ -54,22 +52,15 @@ class UI(object): def options(self): return self.__options - @property - def args(self): - return self.__args + def start(self, extra_args=None): + args = deluge.common.unicode_argv()[1:] + if extra_args: + args.extend(extra_args) - def start(self, args=None): - if args is None: - # Make sure all arguments are unicode - args = deluge.common.unicode_argv()[1:] - - self.__options, self.__args = self.__parser.parse_args(args) - - log = logging.getLogger(__name__) + self.__options = self.parse_args(args) setproctitle("deluge-%s" % self.__name) log.info("Deluge ui %s", deluge.common.get_version()) log.debug("options: %s", self.__options) - log.debug("args: %s", self.__args) log.info("Starting %s ui..", self.__name) diff --git a/deluge/ui/web/web.py b/deluge/ui/web/web.py index be335c468..f0a07bdb3 100644 --- a/deluge/ui/web/web.py +++ b/deluge/ui/web/web.py @@ -10,13 +10,18 @@ from __future__ import print_function import os -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 +# +# Note: Cannot import twisted.internet.reactor because Web is imported from +# from web/__init__.py loaded by the script entry points defined in setup.py +# + + class WebUI(object): def __init__(self, args): from deluge.ui.web import server @@ -33,36 +38,36 @@ class Web(UI): super(Web, self).__init__("web", *args, **kwargs) self.__server = None - group = OptionGroup(self.parser, "Web Options") - group.add_option("-b", "--base", dest="base", action="store", default=None, - help="Set the base path that the ui is running on (proxying)") + group = self.parser.add_argument_group(_('Web Options')) + + group.add_argument("-b", "--base", metavar="", action="store", default=None, + help="Set the base path that the ui is running on (proxying)") if not (windows_check() or osx_check()): - group.add_option("-d", "--do-not-daemonize", dest="donotdaemonize", action="store_true", default=False, - help="Do not daemonize the web interface") - group.add_option("-P", "--pidfile", dest="pidfile", type="str", action="store", default=None, - help="Use pidfile to store process id") + group.add_argument("-d", "--do-not-daemonize", dest="donotdaemonize", action="store_true", default=False, + help="Do not daemonize the web interface") + group.add_argument("-P", "--pidfile", metavar="", action="store", default=None, + help="Use pidfile to store process id") if not windows_check(): - group.add_option("-U", "--user", dest="user", type="str", action="store", default=None, - help="User to switch to. Only use it when starting as root") - group.add_option("-g", "--group", dest="group", type="str", action="store", default=None, - help="Group to switch to. Only use it when starting as root") - group.add_option("-i", "--interface", dest="interface", action="store", default=None, - type="str", help="Binds the webserver to a specific IP address") - group.add_option("-p", "--port", dest="port", type="int", action="store", default=None, - help="Sets the port to be used for the webserver") - group.add_option("--profile", dest="profile", action="store_true", default=False, - help="Profile the web server code") + group.add_argument("-U", "--user", metavar="", action="store", default=None, + help="User to switch to. Only use it when starting as root") + group.add_argument("-g", "--group", metavar="", action="store", default=None, + help="Group to switch to. Only use it when starting as root") + group.add_argument("-i", "--interface", metavar="", action="store", default=None, + help="Binds the webserver to a specific IP address") + group.add_argument("-p", "--port", metavar="", type=int, action="store", default=None, + help="Sets the port to be used for the webserver") + group.add_argument("--profile", action="store_true", default=False, + help="Profile the web server code") try: import OpenSSL assert OpenSSL.__version__ except ImportError: pass else: - group.add_option("--no-ssl", dest="ssl", action="store_false", - help="Forces the webserver to disable ssl", default=False) - group.add_option("--ssl", dest="ssl", action="store_true", - help="Forces the webserver to use ssl", default=False) - self.parser.add_option_group(group) + group.add_argument("--no-ssl", dest="ssl", action="store_false", + help="Forces the webserver to disable ssl", default=False) + group.add_argument("--ssl", dest="ssl", action="store_true", + help="Forces the webserver to use ssl", default=False) @property def server(self): @@ -73,7 +78,7 @@ class Web(UI): # Steps taken from http://www.faqs.org/faqs/unix-faq/programmer/faq/ # Section 1.7 - if not self.options.ensure_value("donotdaemonize", True): + if self.options.donotdaemonize is not True: # fork() so the parent can exit, returns control to the command line # or shell invoking the program. if os.fork(): @@ -93,12 +98,12 @@ class Web(UI): if self.options.pidfile: open(self.options.pidfile, "wb").write("%d\n" % os.getpid()) - if self.options.ensure_value("group", None): + if self.options.group: if not self.options.group.isdigit(): import grp self.options.group = grp.getgrnam(self.options.group)[2] os.setuid(self.options.group) - if self.options.ensure_value("user", None): + if self.options.user: if not self.options.user.isdigit(): import pwd self.options.user = pwd.getpwnam(self.options.user)[2] @@ -116,8 +121,7 @@ class Web(UI): if self.options.port: self.server.port = self.options.port - if self.options.ensure_value("ssl", None): - self.server.https = self.options.ssl + self.server.https = self.options.ssl def run_server(): self.server.install_signal_handlers() @@ -133,7 +137,7 @@ class Web(UI): profiler.dump_stats(profile_output) print("Profile stats saved to %s" % profile_output) - from twisted.internet import reactor + from twisted.internet import reactor # import here because (see top) reactor.addSystemEventTrigger("before", "shutdown", save_profile_stats) print("Running with profiler...") profiler.runcall(run_server)