From 57c3d2590caf96c23c929c700eed6efaa0b92651 Mon Sep 17 00:00:00 2001 From: Jack Robison Date: Sun, 28 May 2017 16:02:22 -0400 Subject: [PATCH] update arg parsing to use docopt --- lbrynet/lbrynet_daemon/DaemonCLI.py | 185 ++++++++++---------- lbrynet/lbrynet_daemon/auth/server.py | 52 +++--- requirements.txt | 1 + setup.py | 1 + tests/unit/lbrynet_daemon/test_DaemonCLI.py | 20 --- 5 files changed, 119 insertions(+), 140 deletions(-) diff --git a/lbrynet/lbrynet_daemon/DaemonCLI.py b/lbrynet/lbrynet_daemon/DaemonCLI.py index a242c55cd..afab796cf 100644 --- a/lbrynet/lbrynet_daemon/DaemonCLI.py +++ b/lbrynet/lbrynet_daemon/DaemonCLI.py @@ -1,23 +1,68 @@ -import argparse import json import os import sys import colorama - +from docopt import docopt +from collections import OrderedDict from lbrynet import conf from lbrynet.core import utils from lbrynet.lbrynet_daemon.auth.client import JSONRPCException, LBRYAPIClient -from lbrynet.lbrynet_daemon.Daemon import LOADING_WALLET_CODE +from lbrynet.lbrynet_daemon.Daemon import LOADING_WALLET_CODE, Daemon from jsonrpc.common import RPCError from urllib2 import URLError, HTTPError from httplib import UNAUTHORIZED -def main(): - colorama.init() - parser = argparse.ArgumentParser(add_help=False) - _, arguments = parser.parse_known_args() +def remove_brackets(key): + if key.startswith("<") and key.endswith(">"): + return str(key[1:-1]) + return key + +def set_flag_vals(flag_names, parsed_args): + kwargs = OrderedDict() + for key, arg in parsed_args.iteritems(): + if arg is None: + continue + elif key.startswith("--"): + if remove_brackets(key[2:]) not in kwargs: + k = remove_brackets(key[2:]) + kwargs[k] = guess_type(arg) + elif key in flag_names: + if remove_brackets(flag_names[key]) not in kwargs: + kwargs[remove_brackets(flag_names[key])] = guess_type(arg) + elif remove_brackets(key) not in kwargs: + kwargs[remove_brackets(key)] = guess_type(arg) + return kwargs + + +def main(): + if len(sys.argv[1:]): + method, args = sys.argv[1], sys.argv[2:] + else: + print_help() + return + + if method in ['help', '--help', '-h']: + if len(args) == 1: + print_help_for_command(args[0]) + else: + print_help() + return + + if method not in Daemon.callable_methods: + print_error("\"%s\" is not a valid command." % method) + return + + fn = Daemon.callable_methods[method] + if hasattr(fn, "_flags"): + flag_names = fn._flags + else: + flag_names = {} + + parsed = docopt(fn.__doc__, args) + kwargs = set_flag_vals(flag_names, parsed) + colorama.init() conf.initialize_settings() api = LBRYAPIClient.get_client() @@ -44,94 +89,43 @@ def main(): print " Status: " + message return 1 - if len(arguments) < 1: - print_help(api) - return 1 - - method = arguments[0] - try: - params = parse_params(arguments[1:]) - except InvalidParameters as e: - print_error(e.message) - return 1 - # TODO: check if port is bound. Error if its not - if method in ['--help', '-h', 'help']: - if len(params) == 0: - print_help(api) - elif 'command' not in params: - print_error( - 'To get help on a specific command, use `{} help command=COMMAND_NAME`'.format( - os.path.basename(sys.argv[0])) - ) + try: + result = api.call(method, **kwargs) + if isinstance(result, basestring): + # printing the undumped string is prettier + print result else: - print_help_for_command(api, params['command']) + print utils.json_dumps_pretty(result) + except (RPCError, KeyError, JSONRPCException, HTTPError) as err: + error_data = None + if isinstance(err, HTTPError): + error_body = err.read() + try: + error_data = json.loads(error_body) + except ValueError: + print ( + "There was an error, and the response was not valid JSON.\n" + + "Raw JSONRPC response:\n" + error_body + ) + return 1 - elif method not in api.commands(): - print_error("'" + method + "' is not a valid command.") + print_error(error_data['error']['message'] + "\n", suggest_help=False) + else: + print_error("Something went wrong\n", suggest_help=False) - else: - try: - result = api.call(method, params) - if isinstance(result, basestring): - # printing the undumped string is prettier - print result - else: - print utils.json_dumps_pretty(result) - except (RPCError, KeyError, JSONRPCException, HTTPError) as err: - error_data = None - if isinstance(err, HTTPError): - error_body = err.read() - try: - error_data = json.loads(error_body) - except ValueError: - print ( - "There was an error, and the response was not valid JSON.\n" + - "Raw JSONRPC response:\n" + error_body - ) - return 1 + print_help_for_command(method) - print_error(error_data['error']['message'] + "\n", suggest_help=False) - else: - print_error("Something went wrong\n", suggest_help=False) - - print_help_for_command(api, method) - if 'data' in error_data['error'] and 'traceback' in error_data['error']['data']: - print "Here's the traceback for the error you encountered:" - print "\n".join(error_data['error']['data']['traceback']) - return 1 - - -def parse_params(params): - if len(params) > 1: - return get_params_from_kwargs(params) - elif len(params) == 1: - try: - return json.loads(params[0]) - except ValueError: - return get_params_from_kwargs(params) - else: - return {} - - -class InvalidParameters(Exception): - pass - - -def get_params_from_kwargs(params): - params_for_return = {} - for i in params: - try: - eq_pos = i.index('=') - except ValueError: - raise InvalidParameters('{} is not in = format'.format(i)) - k, v = i[:eq_pos], i[eq_pos + 1:] - params_for_return[k] = guess_type(v) - return params_for_return + if 'data' in error_data['error'] and 'traceback' in error_data['error']['data']: + print "Here's the traceback for the error you encountered:" + print "\n".join(error_data['error']['data']['traceback']) + return 1 def guess_type(x): + if not isinstance(x, (unicode, str)): + return x if x in ('true', 'True', 'TRUE'): return True if x in ('false', 'False', 'FALSE'): @@ -159,7 +153,8 @@ def print_error(message, suggest_help=True): print_help_suggestion() -def print_help(api): +def print_help(): + commands = Daemon.callable_methods.keys() print "\n".join([ "NAME", " lbrynet-cli - LBRY command line client.", @@ -170,20 +165,18 @@ def print_help(api): "EXAMPLES", " lbrynet-cli commands # list available commands", " lbrynet-cli status # get daemon status", - " lbrynet-cli resolve_name name=what # resolve a name", - " lbrynet-cli help command=resolve_name # get help for a command", + " lbrynet-cli resolve_name what # resolve a name", + " lbrynet-cli help resolve_name # get help for a command", "", "COMMANDS", - wrap_list_to_term_width(api.commands(), prefix=' ') + wrap_list_to_term_width(commands, prefix=' ') ]) -def print_help_for_command(api, command): - help_response = api.call('help', {'command': command}) - print "Help for %s method:" % command - message = help_response['help'] if 'help' in help_response else help_response - message = "\n".join([' ' + line for line in message.split("\n")]) - print message +def print_help_for_command(command): + fn = Daemon.callable_methods.get(command) + if fn: + print "Help for %s method:\n%s" % (command, fn.__doc__) def wrap_list_to_term_width(l, width=None, separator=', ', prefix=''): diff --git a/lbrynet/lbrynet_daemon/auth/server.py b/lbrynet/lbrynet_daemon/auth/server.py index 5c17eace2..3d5751035 100644 --- a/lbrynet/lbrynet_daemon/auth/server.py +++ b/lbrynet/lbrynet_daemon/auth/server.py @@ -1,7 +1,7 @@ import logging import urlparse -import inspect import json +import inspect from decimal import Decimal from zope.interface import implements @@ -15,9 +15,9 @@ from traceback import format_exc from lbrynet import conf from lbrynet.core.Error import InvalidAuthenticationToken from lbrynet.core import utils -from lbrynet.undecorated import undecorated from lbrynet.lbrynet_daemon.auth.util import APIKey, get_auth_message from lbrynet.lbrynet_daemon.auth.client import LBRY_SECRET +from lbrynet.undecorated import undecorated log = logging.getLogger(__name__) @@ -338,12 +338,16 @@ class AuthJSONRPCServer(AuthorizedBase): if args == EMPTY_PARAMS or args == []: args_dict = {} + _args, _kwargs = (), {} elif isinstance(args, dict): args_dict = args elif len(args) == 1 and isinstance(args[0], dict): # TODO: this is for backwards compatibility. Remove this once API and UI are updated # TODO: also delete EMPTY_PARAMS then args_dict = args[0] + _args, _kwargs = (), args + elif isinstance(args, list): + _args, _kwargs = args, {} else: # d = defer.maybeDeferred(function, *args) # if we want to support positional args too raise ValueError('Args must be a dict') @@ -400,28 +404,6 @@ class AuthJSONRPCServer(AuthorizedBase): (utils.now() - time_in).total_seconds())) return server.NOT_DONE_YET - @staticmethod - def _check_params(function, args_dict): - argspec = inspect.getargspec(undecorated(function)) - num_optional_params = 0 if argspec.defaults is None else len(argspec.defaults) - missing_required_params = [ - required_param - for required_param in argspec.args[1:-num_optional_params] - if required_param not in args_dict - ] - if len(missing_required_params): - return 'Missing required parameters', missing_required_params - - extraneous_params = [] if argspec.keywords is not None else [ - extra_param - for extra_param in args_dict - if extra_param not in argspec.args[1:] - ] - if len(extraneous_params): - return 'Extraneous parameters', extraneous_params - - return None, None - def _register_user_session(self, session_id): """ Add or update a HMAC secret for a session @@ -502,6 +484,28 @@ class AuthJSONRPCServer(AuthorizedBase): self._verify_method_is_callable(function_path) return self.callable_methods.get(function_path) + @staticmethod + def _check_params(function, args_dict): + argspec = inspect.getargspec(undecorated(function)) + num_optional_params = 0 if argspec.defaults is None else len(argspec.defaults) + missing_required_params = [ + required_param + for required_param in argspec.args[1:-num_optional_params] + if required_param not in args_dict + ] + if len(missing_required_params): + return 'Missing required parameters', missing_required_params + + extraneous_params = [] if argspec.keywords is not None else [ + extra_param + for extra_param in args_dict + if extra_param not in argspec.args[1:] + ] + if len(extraneous_params): + return 'Extraneous parameters', extraneous_params + + return None, None + def _initialize_session(self, session_id): if not self.sessions.get(session_id, False): self._register_user_session(session_id) diff --git a/requirements.txt b/requirements.txt index ff67ac801..209d09f37 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ Twisted==16.6.0 appdirs==1.4.3 argparse==1.2.1 +docopt==0.6.2 base58==0.2.2 git+https://github.com/lbryio/bumpversion.git#egg=bumpversion colorama==0.3.7 diff --git a/setup.py b/setup.py index 6958ea4ca..1654ab1f8 100644 --- a/setup.py +++ b/setup.py @@ -31,6 +31,7 @@ requires = [ 'seccure', 'txJSON-RPC', 'zope.interface', + 'docopt' ] console_scripts = [ diff --git a/tests/unit/lbrynet_daemon/test_DaemonCLI.py b/tests/unit/lbrynet_daemon/test_DaemonCLI.py index ac5ae4190..fc1b2dcb8 100644 --- a/tests/unit/lbrynet_daemon/test_DaemonCLI.py +++ b/tests/unit/lbrynet_daemon/test_DaemonCLI.py @@ -16,23 +16,3 @@ class DaemonCLITests(unittest.TestCase): self.assertEqual(False, DaemonCLI.guess_type('false')) self.assertEqual(False, DaemonCLI.guess_type('False')) - def test_get_params(self): - test_params = [ - 'b64address=VdNmakxFORPSyfCprAD/eDDPk5TY9QYtSA==', - 'name=test', - 'amount=5.3', - 'n=5', - 'address=bY13xeAjLrsjP4KGETwStK2a9UgKgXVTXu', - 't=true', - 'f=False', - ] - test_r = { - 'b64address': 'VdNmakxFORPSyfCprAD/eDDPk5TY9QYtSA==', - 'name': 'test', - 'amount': 5.3, - 'n': 5, - 'address': 'bY13xeAjLrsjP4KGETwStK2a9UgKgXVTXu', - 't': True, - 'f': False, - } - self.assertDictEqual(test_r, DaemonCLI.get_params_from_kwargs(test_params))