From 1fd521956f6aaf7cb00e345038578c3bd134c12a Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Tue, 29 Sep 2026 09:23:30 +0000 Subject: [PATCH] main_tool: relocate init funcs to `tool.__init__` --- mmgen/main_tool.py | 126 +----------------------------------- mmgen/tool/__init__.py | 144 +++++++++++++++++++++++++++++++++++++++++ mmgen/tool/help.py | 5 +- test/tooltest.py | 5 +- test/tooltest2.py | 5 +- 5 files changed, 155 insertions(+), 130 deletions(-) mode change 100644 => 100755 mmgen/tool/__init__.py diff --git a/mmgen/main_tool.py b/mmgen/main_tool.py index 178245bb..5cdee1c7 100755 --- a/mmgen/main_tool.py +++ b/mmgen/main_tool.py @@ -23,8 +23,8 @@ mmgen-tool: Perform various MMGen- and cryptocoin-related operations. import sys, os, importlib from .cfg import gc, Config -from .util import msg, Msg, die, capfirst, suf, async_run, isAsync -from .tool.common import fstr +from .util import Msg, die, capfirst, async_run, isAsync +from .tool import process_args opts_data = { 'filter_codes': ['-'], @@ -186,128 +186,6 @@ mods = { ), } -def get_cmds(): - return [cmd for mod, cmds in mods.items() if mod != 'help' for cmd in cmds] - -def create_call_sig(cmd, cls, *, as_string=False): - - m = getattr(cls, cmd) - - if 'varargs_call_sig' in m.__code__.co_varnames: # hack - flag = 'VAR_ARGS' - va = m.__defaults__[0] - args, dfls, ann = va['args'], va['dfls'], va['annots'] - else: - flag = None - c = m.__code__ - args = c.co_varnames[1:c.co_argcount + c.co_posonlyargcount + c.co_kwonlyargcount] - dfls = ( - (m.__defaults__ or ()) + - tuple(m.__kwdefaults__[k] for k in args if k in (m.__kwdefaults__ or ()))) - ann = m.__annotations__ - - nargs = len(args) - len(dfls) - dfl_types = tuple( - ann[a] if a in ann and isinstance(ann[a], type) else type(dfls[i]) - for i, a in enumerate(args[nargs:])) - - if as_string: - get_type_from_ann = lambda x: 'str or STDIN' if ann[x] == fstr else ann[x].__name__ - return ' '.join( - [f'{a} [{get_type_from_ann(a)}]' for a in args[:nargs]] + - [f'{a} [{dfl_types[n].__name__}={dfls[n]!r}]' for n, a in enumerate(args[nargs:])]) - else: - get_type_from_ann = lambda x: 'str' if ann[x] == fstr else ann[x].__name__ - return ( - [(a, get_type_from_ann(a)) for a in args[:nargs]], # c_args - {a: dfls[n] for n, a in enumerate(args[nargs:])}, # c_kwargs - {a: dfl_types[n] for n, a in enumerate(args[nargs:])}, # c_kwargs_types - ('STDIN_OK' if nargs and ann[args[0]] == fstr else flag), # flag - ann) # ann - -def process_args(cmd, cmd_args, cls): - c_args, c_kwargs, c_kwargs_types, flag, _ = create_call_sig(cmd, cls) - have_stdin_input = False - - def usage_die(s): - msg(s) - from .tool.help import usage - usage(cmd) - - if flag != 'VAR_ARGS': - if len(cmd_args) < len(c_args): - usage_die(f'Command requires exactly {len(c_args)} non-keyword argument{suf(c_args)}') - - u_args = cmd_args[:len(c_args)] - - # If we're reading from a pipe, replace '-' with output of previous command - if flag == 'STDIN_OK' and u_args and u_args[0] == '-': - if sys.stdin.isatty(): - die('BadFilename', "Standard input is a TTY. Can't use '-' as a filename") - else: - from .util2 import parse_bytespec - max_dlen_spec = '10kB' # limit input to 10KB for now - max_dlen = parse_bytespec(max_dlen_spec) - u_args[0] = os.read(0, max_dlen) - have_stdin_input = True - if len(u_args[0]) >= max_dlen: - die(2, f'Maximum data input for this command is {max_dlen_spec}') - if not u_args[0]: - die(2, f'{cmd}: ERROR: no output from previous command in pipe') - - u_nkwargs = len(cmd_args) - len(c_args) - u_kwargs = {} - if flag == 'VAR_ARGS': - cmd_args = ['dummy_arg'] + cmd_args - t = [a.split('=', 1) for a in cmd_args if '=' in a] - tk = [a[0] for a in t] - tk_bad = [a for a in tk if a not in c_kwargs] - if set(tk_bad) != set(tk[:len(tk_bad)]): # permit non-kw args to contain '=' - die(1, f'{tk_bad[-1]!r}: illegal keyword argument') - u_kwargs = dict(t[len(tk_bad):]) - u_args = cmd_args[:-len(u_kwargs) or None] - elif u_nkwargs > 0: - u_kwargs = dict([a.split('=', 1) for a in cmd_args[len(c_args):] if '=' in a]) - if len(u_kwargs) != u_nkwargs: - usage_die(f'Command requires exactly {len(c_args)} non-keyword argument{suf(c_args)}') - if len(u_kwargs) > len(c_kwargs): - usage_die(f'Command accepts no more than {len(c_kwargs)} keyword argument{suf(c_kwargs)}') - - for k in u_kwargs: - if k not in c_kwargs: - usage_die(f'{k!r}: invalid keyword argument') - - def conv_type(arg, arg_name, arg_type): - if arg_type == 'bytes' and not isinstance(arg, bytes): - die(1, "'Binary input data must be supplied via STDIN") - - if have_stdin_input and arg_type == 'str' and isinstance(arg, bytes): - NL = '\r\n' if gc.platform == 'win32' else '\n' - arg = arg.decode() - if arg[-len(NL):] == NL: # rstrip one newline - arg = arg[:-len(NL)] - - if arg_type == 'bool': - if arg.lower() in ('true', 'yes', '1', 'on'): - arg = True - elif arg.lower() in ('false', 'no', '0', 'off'): - arg = False - else: - usage_die(f'{arg!r}: invalid boolean value for keyword argument') - - try: - return __builtins__[arg_type](arg) - except: - die(1, f'{arg!r}: Invalid argument for argument {arg_name} ({arg_type!r} required)') - - if flag == 'VAR_ARGS': - args = [conv_type(u_args[i], c_args[0][0], c_args[0][1]) for i in range(len(u_args))] - else: - args = [conv_type(u_args[i], c_args[i][0], c_args[i][1]) for i in range(len(c_args))] - kwargs = {k: conv_type(v, k, c_kwargs_types[k].__name__) for k, v in u_kwargs.items()} - - return (args, kwargs) - def process_result(ret, *, pager=False, print_result=False): """ Convert result to something suitable for output to screen and return it. diff --git a/mmgen/tool/__init__.py b/mmgen/tool/__init__.py old mode 100644 new mode 100755 index 31c8228b..5148237f --- a/mmgen/tool/__init__.py +++ b/mmgen/tool/__init__.py @@ -1,4 +1,148 @@ +#!/usr/bin/env python3 +# +# MMGen Wallet, a terminal-based cryptocurrency wallet +# Copyright (C)2013-2026 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen-wallet +# https://gitlab.com/mmgen/mmgen-wallet + +""" +tool.__init__: initialization functions for the ‘mmgen-tool’ utility and others +""" + +import sys, os + +from ..cfg import gc +from ..util import die + +from .common import fstr + # provide this for backwards compatibility: def tool_api(*args, **kwargs): from .api import tool_api return tool_api(*args, **kwargs) + +def get_cmds(mods): + return [cmd for mod, cmds in mods.items() if mod != 'help' for cmd in cmds] + +def create_call_sig(cmd, cls, *, as_string=False): + + m = getattr(cls, cmd) + + if 'varargs_call_sig' in m.__code__.co_varnames: # hack + flag = 'VAR_ARGS' + va = m.__defaults__[0] + args, dfls, ann = va['args'], va['dfls'], va['annots'] + else: + flag = None + c = m.__code__ + args = c.co_varnames[1:c.co_argcount + c.co_posonlyargcount + c.co_kwonlyargcount] + dfls = ( + (m.__defaults__ or ()) + + tuple(m.__kwdefaults__[k] for k in args if k in (m.__kwdefaults__ or ()))) + ann = m.__annotations__ + + nargs = len(args) - len(dfls) + dfl_types = tuple( + ann[a] if a in ann and isinstance(ann[a], type) else type(dfls[i]) + for i, a in enumerate(args[nargs:])) + + if as_string: + get_type_from_ann = lambda x: 'str or STDIN' if ann[x] == fstr else ann[x].__name__ + return ' '.join( + [f'{a} [{get_type_from_ann(a)}]' for a in args[:nargs]] + + [f'{a} [{dfl_types[n].__name__}={dfls[n]!r}]' for n, a in enumerate(args[nargs:])]) + else: + get_type_from_ann = lambda x: 'str' if ann[x] == fstr else ann[x].__name__ + return ( + [(a, get_type_from_ann(a)) for a in args[:nargs]], # c_args + {a: dfls[n] for n, a in enumerate(args[nargs:])}, # c_kwargs + {a: dfl_types[n] for n, a in enumerate(args[nargs:])}, # c_kwargs_types + ('STDIN_OK' if nargs and ann[args[0]] == fstr else flag), # flag + ann) # ann + +def process_args(cmd, cmd_args, cls): + c_args, c_kwargs, c_kwargs_types, flag, _ = create_call_sig(cmd, cls) + have_stdin_input = False + from ..util import msg, suf + + def usage_die(s): + msg(s) + from .help import usage + usage(cmd) + + if flag != 'VAR_ARGS': + if len(cmd_args) < len(c_args): + usage_die(f'Command requires exactly {len(c_args)} non-keyword argument{suf(c_args)}') + + u_args = cmd_args[:len(c_args)] + + # If we're reading from a pipe, replace '-' with output of previous command + if flag == 'STDIN_OK' and u_args and u_args[0] == '-': + if sys.stdin.isatty(): + die('BadFilename', "Standard input is a TTY. Can't use '-' as a filename") + else: + from ..util2 import parse_bytespec + max_dlen_spec = '10kB' # limit input to 10KB for now + max_dlen = parse_bytespec(max_dlen_spec) + u_args[0] = os.read(0, max_dlen) + have_stdin_input = True + if len(u_args[0]) >= max_dlen: + die(2, f'Maximum data input for this command is {max_dlen_spec}') + if not u_args[0]: + die(2, f'{cmd}: ERROR: no output from previous command in pipe') + + u_nkwargs = len(cmd_args) - len(c_args) + u_kwargs = {} + if flag == 'VAR_ARGS': + cmd_args = ['dummy_arg'] + cmd_args + t = [a.split('=', 1) for a in cmd_args if '=' in a] + tk = [a[0] for a in t] + tk_bad = [a for a in tk if a not in c_kwargs] + if set(tk_bad) != set(tk[:len(tk_bad)]): # permit non-kw args to contain '=' + die(1, f'{tk_bad[-1]!r}: illegal keyword argument') + u_kwargs = dict(t[len(tk_bad):]) + u_args = cmd_args[:-len(u_kwargs) or None] + elif u_nkwargs > 0: + u_kwargs = dict([a.split('=', 1) for a in cmd_args[len(c_args):] if '=' in a]) + if len(u_kwargs) != u_nkwargs: + usage_die(f'Command requires exactly {len(c_args)} non-keyword argument{suf(c_args)}') + if len(u_kwargs) > len(c_kwargs): + usage_die(f'Command accepts no more than {len(c_kwargs)} keyword argument{suf(c_kwargs)}') + + for k in u_kwargs: + if k not in c_kwargs: + usage_die(f'{k!r}: invalid keyword argument') + + def conv_type(arg, arg_name, arg_type): + if arg_type == 'bytes' and not isinstance(arg, bytes): + die(1, "'Binary input data must be supplied via STDIN") + + if have_stdin_input and arg_type == 'str' and isinstance(arg, bytes): + NL = '\r\n' if gc.platform == 'win32' else '\n' + arg = arg.decode() + if arg[-len(NL):] == NL: # rstrip one newline + arg = arg[:-len(NL)] + + if arg_type == 'bool': + if arg.lower() in ('true', 'yes', '1', 'on'): + arg = True + elif arg.lower() in ('false', 'no', '0', 'off'): + arg = False + else: + usage_die(f'{arg!r}: invalid boolean value for keyword argument') + + try: + return __builtins__[arg_type](arg) + except: + die(1, f'{arg!r}: Invalid argument for argument {arg_name} ({arg_type!r} required)') + + if flag == 'VAR_ARGS': + args = [conv_type(u_args[i], c_args[0][0], c_args[0][1]) for i in range(len(u_args))] + else: + args = [conv_type(u_args[i], c_args[i][0], c_args[i][1]) for i in range(len(c_args))] + kwargs = {k: conv_type(v, k, c_kwargs_types[k].__name__) for k, v in u_kwargs.items()} + + return (args, kwargs) diff --git a/mmgen/tool/help.py b/mmgen/tool/help.py index c94eea57..02708b0b 100755 --- a/mmgen/tool/help.py +++ b/mmgen/tool/help.py @@ -23,6 +23,7 @@ tool.help: Help screen routines for the 'mmgen-tool' utility from .. import main_tool from .common import tool_cmd_base, fstr +from . import create_call_sig usage_hdr = """ GENERAL USAGE INFORMATION FOR MMGEN-TOOL COMMANDS @@ -124,7 +125,7 @@ def gen_tool_usage(): for cmdname in cmdlist: yield ' {a:{w}} {b}'.format( a = cmdname, - b = main_tool.create_call_sig(cmdname, cls, as_string=True), + b = create_call_sig(cmdname, cls, as_string=True), w = max_w) yield '' @@ -138,7 +139,7 @@ def gen_tool_cmd_usage(mod, cmdname): cls = main_tool.get_mod_cls(mod) docstr = getattr(cls, cmdname).__doc__.strip() - args, kwargs, kwargs_types, _, ann = main_tool.create_call_sig(cmdname, cls) + args, kwargs, kwargs_types, _, ann = create_call_sig(cmdname, cls) ARGS = 'ARG' if len(args) == 1 else 'ARGS' if args else '' KWARGS = 'KEYWORD ARG' if len(kwargs) == 1 else 'KEYWORD ARGS' if kwargs else '' diff --git a/test/tooltest.py b/test/tooltest.py index 29ac20ba..b2469c24 100755 --- a/test/tooltest.py +++ b/test/tooltest.py @@ -202,9 +202,10 @@ if cfg.testing_status: Msg(' '+'\n '.join(sorted(tested_in[l]))) ignore = () - from mmgen.main_tool import get_cmds + from mmgen.tool import get_cmds + from mmgen.main_tool import mods uc = sorted( - set(get_cmds()) - + set(get_cmds(mods)) - set(ignore) - set(tested_in['tooltest.py']) - set(tested_in['tooltest2.py']) - diff --git a/test/tooltest2.py b/test/tooltest2.py index 045a56bb..92b88b82 100755 --- a/test/tooltest2.py +++ b/test/tooltest2.py @@ -36,6 +36,7 @@ except ImportError: from test.include.common import set_globals, end_msg, init_coverage from mmgen import main_tool +from mmgen.tool import process_args from mmgen.cfg import Config, gc from mmgen.color import green, blue, purple, cyan, gray from mmgen.util import msg, msg_r, Msg, die, isAsync @@ -112,7 +113,7 @@ def call_method(cls, method, cmd_name, args, mmtype, stdin_input): a = purple('Running'), b = ' '.join([cmd_name]+[repr(e) for e in args]), c = ' '+mmtype if mmtype else '')) - aargs, kwargs = main_tool.process_args(cmd_name, args, cls) + aargs, kwargs = process_args(cmd_name, args, cls) oq_save = bool(cfg.quiet) if not cfg.verbose: cfg._set_quiet(True) @@ -144,7 +145,7 @@ def tool_api(cls, cmd_name, args, opts): for o in opts: if o.startswith('--type='): tool.addrtype = o.split('=')[1] - pargs, kwargs = main_tool.process_args(cmd_name, args, cls) + pargs, kwargs = process_args(cmd_name, args, cls) return getattr(tool, cmd_name)(*pargs, **kwargs) def check_output(out, chk):