forked from LBRYCommunity/lbry-sdk
cli bug fixes
This commit is contained in:
parent
eba88c1df7
commit
7a038bbb98
3 changed files with 176 additions and 112 deletions
|
@ -115,20 +115,37 @@ def set_kwargs(parsed_args):
|
|||
return kwargs
|
||||
|
||||
|
||||
class ArgumentParser(argparse.ArgumentParser):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, add_help=False, **kwargs)
|
||||
self.add_argument(
|
||||
'--help', dest='help', action='store_true', default=False,
|
||||
help='show this help message and exit'
|
||||
)
|
||||
|
||||
|
||||
def add_command_parser(parent, command):
|
||||
subcommand = parent.add_parser(
|
||||
command['name'],
|
||||
help=command['doc'].strip().splitlines()[0]
|
||||
)
|
||||
subcommand.set_defaults(
|
||||
api_method_name=command['api_method_name'],
|
||||
command=command['name'],
|
||||
doc=command['doc'],
|
||||
replaced_by=command.get('replaced_by', None)
|
||||
)
|
||||
|
||||
|
||||
def get_argument_parser():
|
||||
main = argparse.ArgumentParser('lbrynet', add_help=False)
|
||||
main = ArgumentParser('lbrynet')
|
||||
main.add_argument(
|
||||
'--version', dest='cli_version', action="store_true",
|
||||
help='Show lbrynet CLI version and exit.'
|
||||
)
|
||||
main.add_argument(
|
||||
'-h', '--help', dest='help', action="store_true",
|
||||
help='Show this help message and exit'
|
||||
)
|
||||
main.set_defaults(group=None, command=None)
|
||||
CLIConfig.contribute_args(main)
|
||||
sub = main.add_subparsers(dest='command')
|
||||
help = sub.add_parser('help', help='Detailed help for remote commands.')
|
||||
help.add_argument('help_command', nargs='*')
|
||||
sub = main.add_subparsers()
|
||||
start = sub.add_parser('start', help='Start lbrynet server.')
|
||||
start.add_argument(
|
||||
'--quiet', dest='quiet', action="store_true",
|
||||
|
@ -139,19 +156,22 @@ def get_argument_parser():
|
|||
help=('Enable debug output. Optionally specify loggers for which debug output '
|
||||
'should selectively be applied.')
|
||||
)
|
||||
start.set_defaults(command='start', start_parser=start)
|
||||
Config.contribute_args(start)
|
||||
|
||||
api = Daemon.get_api_definitions()
|
||||
for group in sorted(api):
|
||||
group_command = sub.add_parser(group, help=api[group]['doc'])
|
||||
group_command.set_defaults(group_doc=group_command)
|
||||
if group in ('status', 'publish', 'version', 'help', 'wallet_balance', 'get'):
|
||||
continue
|
||||
commands = group_command.add_subparsers(dest='subcommand')
|
||||
for command in api[group]['commands']:
|
||||
commands.add_parser(command['name'], help=command['doc'].strip().splitlines()[0])
|
||||
for deprecated in Daemon.deprecated_methods:
|
||||
group_command = sub.add_parser(deprecated)
|
||||
group_command.add_subparsers(dest='subcommand')
|
||||
groups = {}
|
||||
for group_name in sorted(api['groups']):
|
||||
group_parser = sub.add_parser(group_name, help=api['groups'][group_name])
|
||||
group_parser.set_defaults(group=group_name, group_parser=group_parser)
|
||||
groups[group_name] = group_parser.add_subparsers()
|
||||
for command_name in sorted(api['commands']):
|
||||
command = api['commands'][command_name]
|
||||
if command['group'] is None:
|
||||
add_command_parser(sub, command)
|
||||
else:
|
||||
add_command_parser(groups[command['group']], command)
|
||||
|
||||
return main
|
||||
|
||||
|
||||
|
@ -168,6 +188,10 @@ def main(argv=None):
|
|||
|
||||
elif args.command == 'start':
|
||||
|
||||
if args.help:
|
||||
args.start_parser.print_help()
|
||||
return 0
|
||||
|
||||
log_support.configure_logging(conf.log_file_path, not args.quiet, args.verbose)
|
||||
|
||||
if conf.share_usage_data:
|
||||
|
@ -187,44 +211,25 @@ def main(argv=None):
|
|||
else:
|
||||
log.info("Not connected to internet, unable to start")
|
||||
|
||||
elif args.command == 'help':
|
||||
|
||||
if args.help_command:
|
||||
method = '_'.join(args.help_command)
|
||||
else:
|
||||
parser.print_help()
|
||||
return 0
|
||||
|
||||
if method not in Daemon.callable_methods:
|
||||
print('Invalid command name: {method}')
|
||||
return 1
|
||||
|
||||
fn = Daemon.callable_methods[method]
|
||||
print(fn.__doc__)
|
||||
|
||||
elif args.command is not None:
|
||||
|
||||
if args.command in ('status', 'publish', 'version', 'help', 'wallet_balance', 'get'):
|
||||
method = args.command
|
||||
elif args.subcommand is not None:
|
||||
method = f'{args.command}_{args.subcommand}'
|
||||
doc = args.doc
|
||||
api_method_name = args.api_method_name
|
||||
if args.replaced_by:
|
||||
print(f"{args.api_method_name} is deprecated, using {args.replaced_by['api_method_name']}.")
|
||||
doc = args.replaced_by['doc']
|
||||
api_method_name = args.replaced_by['api_method_name']
|
||||
|
||||
if args.help:
|
||||
print(doc)
|
||||
else:
|
||||
args.group_doc.print_help()
|
||||
return 0
|
||||
parsed = docopt(doc, command_args)
|
||||
params = set_kwargs(parsed)
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(execute_command(conf, api_method_name, params))
|
||||
|
||||
if method in Daemon.deprecated_methods:
|
||||
new_method = Daemon.deprecated_methods[method].new_command
|
||||
if new_method is None:
|
||||
print(f"{method} is permanently deprecated and does not have a replacement command.")
|
||||
return 0
|
||||
print(f"{method} is deprecated, using {new_method}.")
|
||||
method = new_method
|
||||
|
||||
fn = Daemon.callable_methods[method]
|
||||
parsed = docopt(fn.__doc__, command_args)
|
||||
params = set_kwargs(parsed)
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(execute_command(conf, method, params))
|
||||
elif args.group is not None:
|
||||
args.group_parser.print_help()
|
||||
|
||||
else:
|
||||
parser.print_help()
|
||||
|
|
|
@ -399,21 +399,50 @@ class Daemon(metaclass=JSONRPCServerType):
|
|||
|
||||
@classmethod
|
||||
def get_api_definitions(cls):
|
||||
groups = {}
|
||||
for method in dir(cls):
|
||||
if method.startswith('jsonrpc_'):
|
||||
parts = method.split('_', 2)
|
||||
group = command = parts[1]
|
||||
if len(parts) == 3:
|
||||
command = parts[2]
|
||||
group_dict = {'doc': getattr(cls, f'{group.upper()}_DOC', ''), 'commands': []}
|
||||
groups.setdefault(group, group_dict)['commands'].append({
|
||||
'name': command,
|
||||
'doc': getattr(cls, method).__doc__
|
||||
})
|
||||
del groups['commands']
|
||||
del groups['help']
|
||||
return groups
|
||||
prefix = 'jsonrpc_'
|
||||
skip = ['commands', 'help']
|
||||
not_grouped = ['block_show', 'report_bug', 'resolve_name', 'routing_table_get']
|
||||
api = {
|
||||
'groups': {
|
||||
group_name[:-len('_DOC')].lower(): getattr(cls, group_name).strip()
|
||||
for group_name in dir(cls) if group_name.endswith('_DOC')
|
||||
},
|
||||
'commands': {}
|
||||
}
|
||||
for jsonrpc_method in dir(cls):
|
||||
if jsonrpc_method.startswith(prefix):
|
||||
full_name = jsonrpc_method[len(prefix):]
|
||||
if full_name in skip:
|
||||
continue
|
||||
method = getattr(cls, jsonrpc_method)
|
||||
if full_name in not_grouped:
|
||||
name_parts = [full_name]
|
||||
else:
|
||||
name_parts = full_name.split('_', 1)
|
||||
if len(name_parts) == 1:
|
||||
group = None
|
||||
name, = name_parts
|
||||
elif len(name_parts) == 2:
|
||||
group, name = name_parts
|
||||
assert group in api['groups'],\
|
||||
f"Group {group} does not have doc string for command {full_name}."
|
||||
else:
|
||||
raise NameError(f'Could not parse method name: {jsonrpc_method}')
|
||||
api['commands'][full_name] = {
|
||||
'api_method_name': full_name,
|
||||
'name': name,
|
||||
'group': group,
|
||||
'doc': method.__doc__,
|
||||
'method': method,
|
||||
}
|
||||
if hasattr(method, '_deprecated'):
|
||||
api['commands'][full_name]['replaced_by'] = method.new_command
|
||||
|
||||
for command in api['commands'].values():
|
||||
if 'replaced_by' in command:
|
||||
command['replaced_by'] = api['commands'][command['replaced_by']]
|
||||
|
||||
return api
|
||||
|
||||
@property
|
||||
def db_revision_file_path(self):
|
||||
|
@ -1319,6 +1348,10 @@ class Daemon(metaclass=JSONRPCServerType):
|
|||
"""
|
||||
return sorted([command for command in self.callable_methods.keys()])
|
||||
|
||||
WALLET_DOC = """
|
||||
Wallet management.
|
||||
"""
|
||||
|
||||
@deprecated("account_balance")
|
||||
def jsonrpc_wallet_balance(self, address=None):
|
||||
""" deprecated """
|
||||
|
@ -2249,6 +2282,9 @@ class Daemon(metaclass=JSONRPCServerType):
|
|||
log.info("Deleted file: %s", file_name)
|
||||
return True
|
||||
|
||||
STREAM_DOC = """
|
||||
Stream information.
|
||||
"""
|
||||
@requires(WALLET_COMPONENT, EXCHANGE_RATE_MANAGER_COMPONENT, BLOB_COMPONENT,
|
||||
DHT_COMPONENT, RATE_LIMITER_COMPONENT, PAYMENT_RATE_COMPONENT, DATABASE_COMPONENT,
|
||||
conditions=[WALLET_IS_UNLOCKED])
|
||||
|
@ -2909,7 +2945,7 @@ class Daemon(metaclass=JSONRPCServerType):
|
|||
results[u]['claims_in_channel'] = resolved[u].get('claims_in_channel', [])
|
||||
return results
|
||||
|
||||
CHANNEL_DOC = """
|
||||
TRANSACTION_DOC = """
|
||||
Transaction management.
|
||||
"""
|
||||
|
||||
|
@ -3147,6 +3183,10 @@ class Daemon(metaclass=JSONRPCServerType):
|
|||
await d2f(self.blob_manager.delete_blobs([blob_hash]))
|
||||
return "Deleted %s" % blob_hash
|
||||
|
||||
PEER_DOC = """
|
||||
DHT / Blob Exchange peer commands.
|
||||
"""
|
||||
|
||||
@requires(DHT_COMPONENT)
|
||||
async def jsonrpc_peer_list(self, blob_hash, timeout=None):
|
||||
"""
|
||||
|
|
|
@ -1,13 +1,25 @@
|
|||
import contextlib
|
||||
from io import StringIO
|
||||
from twisted.trial import unittest
|
||||
import unittest
|
||||
|
||||
from docopt import DocoptExit
|
||||
from lbrynet.extras.cli import normalize_value, main
|
||||
from lbrynet.extras.system_info import get_platform
|
||||
|
||||
|
||||
class CLITest(unittest.TestCase):
|
||||
|
||||
@staticmethod
|
||||
def shell(argv):
|
||||
actual_output = StringIO()
|
||||
with contextlib.redirect_stdout(actual_output):
|
||||
with contextlib.redirect_stderr(actual_output):
|
||||
try:
|
||||
main(argv)
|
||||
except SystemExit as e:
|
||||
print(e.args[0])
|
||||
return actual_output.getvalue().strip()
|
||||
|
||||
def test_guess_type(self):
|
||||
self.assertEqual('0.3.8', normalize_value('0.3.8'))
|
||||
self.assertEqual('0.3', normalize_value('0.3'))
|
||||
|
@ -39,58 +51,65 @@ class CLITest(unittest.TestCase):
|
|||
|
||||
self.assertEqual(3, normalize_value('3', key="some_other_thing"))
|
||||
|
||||
def test_help_command(self):
|
||||
actual_output = StringIO()
|
||||
with contextlib.redirect_stdout(actual_output):
|
||||
main(['help'])
|
||||
actual_output = actual_output.getvalue()
|
||||
self.assertSubstring('usage: lbrynet [--version] [-h]', actual_output)
|
||||
|
||||
def test_help_for_command_command(self):
|
||||
actual_output = StringIO()
|
||||
with contextlib.redirect_stdout(actual_output):
|
||||
main(['help', 'publish'])
|
||||
actual_output = actual_output.getvalue()
|
||||
self.assertSubstring('Make a new name claim and publish', actual_output)
|
||||
self.assertSubstring('Usage:', actual_output)
|
||||
|
||||
def test_help_for_command_command_with_invalid_command(self):
|
||||
actual_output = StringIO()
|
||||
with contextlib.redirect_stdout(actual_output):
|
||||
main(['help', 'publish1'])
|
||||
self.assertSubstring('Invalid command name', actual_output.getvalue())
|
||||
|
||||
def test_version_command(self):
|
||||
actual_output = StringIO()
|
||||
with contextlib.redirect_stdout(actual_output):
|
||||
main(['--version'])
|
||||
self.assertEqual(
|
||||
actual_output.getvalue().strip(),
|
||||
"lbrynet {lbrynet_version}".format(**get_platform())
|
||||
def test_help(self):
|
||||
self.assertIn(
|
||||
'usage: lbrynet [--help] [--version] [--api API]', self.shell(['--help'])
|
||||
)
|
||||
# start is special command, with separate help handling
|
||||
self.assertIn(
|
||||
'--share-usage-data', self.shell(['start', '--help'])
|
||||
)
|
||||
# publish is ungrouped command, returns usage only implicitly
|
||||
self.assertIn(
|
||||
'publish (<name> | --name=<name>)', self.shell(['publish'])
|
||||
)
|
||||
# publish is ungrouped command, with explicit --help
|
||||
self.assertIn(
|
||||
'Make a new name claim and publish', self.shell(['publish', '--help'])
|
||||
)
|
||||
# account is a group, returns help implicitly
|
||||
self.assertIn(
|
||||
'{add,balance,create,decrypt,encrypt,fund,list,lock,max_address_gap,remove,send,set,unlock}',
|
||||
self.shell(['account'])
|
||||
)
|
||||
# account is a group, with explicit --help
|
||||
self.assertIn(
|
||||
'{add,balance,create,decrypt,encrypt,fund,list,lock,max_address_gap,remove,send,set,unlock}',
|
||||
self.shell(['account', '--help'])
|
||||
)
|
||||
# account add is a grouped command, returns usage implicitly
|
||||
self.assertIn(
|
||||
'account_add (<account_name> | --account_name=<account_name>)',
|
||||
self.shell(['account', 'add'])
|
||||
)
|
||||
# account add is a grouped command, with explicit --help
|
||||
self.assertIn(
|
||||
'Add a previously created account from a seed,', self.shell(['account', 'add', '--help'])
|
||||
)
|
||||
# help for invalid command, with explicit --help
|
||||
self.assertIn(
|
||||
"invalid choice: 'publish1'", self.shell(['publish1', '--help'])
|
||||
)
|
||||
# help for invalid command, implicit
|
||||
self.assertIn(
|
||||
"invalid choice: 'publish1'", self.shell(['publish1'])
|
||||
)
|
||||
|
||||
def test_invalid_command(self):
|
||||
actual_output = StringIO()
|
||||
with contextlib.redirect_stderr(actual_output):
|
||||
try:
|
||||
main(['publish1'])
|
||||
except SystemExit:
|
||||
pass
|
||||
self.assertSubstring("invalid choice: 'publish1'", actual_output.getvalue())
|
||||
def test_version_command(self):
|
||||
self.assertEqual(
|
||||
"lbrynet {lbrynet_version}".format(**get_platform()), self.shell(['--version'])
|
||||
)
|
||||
|
||||
def test_valid_command_daemon_not_started(self):
|
||||
actual_output = StringIO()
|
||||
with contextlib.redirect_stdout(actual_output):
|
||||
main(["publish", '--name=asd', '--bid=99'])
|
||||
self.assertEqual(
|
||||
actual_output.getvalue().strip(),
|
||||
"Could not connect to daemon. Are you sure it's running?"
|
||||
"Could not connect to daemon. Are you sure it's running?",
|
||||
self.shell(["publish", '--name=asd', '--bid=99'])
|
||||
)
|
||||
|
||||
def test_deprecated_command_daemon_not_started(self):
|
||||
actual_output = StringIO()
|
||||
with contextlib.redirect_stdout(actual_output):
|
||||
main(["wallet_balance"])
|
||||
main(["wallet", "balance"])
|
||||
self.assertEqual(
|
||||
actual_output.getvalue().strip(),
|
||||
"wallet_balance is deprecated, using account_balance.\n"
|
||||
|
|
Loading…
Reference in a new issue