From 068377cf1b466d8085623535b3af73fd8da2d653 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Wed, 8 Apr 2020 08:51:19 +0000 Subject: [PATCH] tool.py: new MMGenToolCmdMeta metaclass Testing: $ test/test.py tool_help $ test/tooltest2.py $ test/tooltest2.py -A $ test/tooltest2.py -f $ test/tooltest.py $ test/test.py tool --- mmgen/main_tool.py | 12 ++++----- mmgen/tool.py | 65 ++++++++++++++++++++++++++++++---------------- test/tooltest.py | 2 +- test/tooltest2.py | 15 +++++------ 4 files changed, 55 insertions(+), 39 deletions(-) diff --git a/mmgen/main_tool.py b/mmgen/main_tool.py index bdd8b3ef..344622f8 100755 --- a/mmgen/main_tool.py +++ b/mmgen/main_tool.py @@ -26,7 +26,7 @@ from mmgen.common import * def make_cmd_help(): import mmgen.tool def make_help(): - for bc in mmgen.tool.MMGenToolCmd.__bases__: + for bc in mmgen.tool.MMGenToolCmds.classes.values(): cls_doc = bc.__doc__.strip().split('\n') for l in cls_doc: if l is cls_doc[0]: @@ -39,10 +39,9 @@ def make_cmd_help(): yield '' yield '' - max_w = max(map(len,bc._user_commands())) + max_w = max(map(len,bc.user_commands)) fs = ' {{:{}}} - {{}}'.format(max_w) - for name in bc._user_commands(): - code = getattr(bc,name) + for name,code in bc.user_commands.items(): if code.__doc__: yield fs.format(name, pretty_format( @@ -98,16 +97,15 @@ if len(cmd_args) < 1: opts.usage() cmd = cmd_args.pop(0) import mmgen.tool as tool -tc = tool.MMGenToolCmd() if cmd in ('help','usage') and cmd_args: cmd_args[0] = 'command_name=' + cmd_args[0] -if cmd not in dir(tc): +if cmd not in tool.MMGenToolCmds: die(1,"'{}': no such command".format(cmd)) args,kwargs = tool._process_args(cmd,cmd_args) -ret = getattr(tc,cmd)(*args,**kwargs) +ret = tool.MMGenToolCmds.call(cmd,*args,**kwargs) tool._process_result(ret,pager='pager' in kwargs and kwargs['pager'],print_result=True) diff --git a/mmgen/tool.py b/mmgen/tool.py index cd273842..8f384d44 100755 --- a/mmgen/tool.py +++ b/mmgen/tool.py @@ -32,7 +32,7 @@ def _options_annot_str(l): def _create_call_sig(cmd,parsed=False): - m = getattr(MMGenToolCmd,cmd) + m = MMGenToolCmds[cmd] if 'varargs_call_sig' in m.__code__.co_varnames: # hack flag = 'VAR_ARGS' @@ -90,16 +90,16 @@ def _usage(cmd=None,exit_val=1): if not cmd: Msg(m1) - for bc in MMGenToolCmd.__bases__: + for bc in MMGenToolCmds.classes.values(): cls_info = bc.__doc__.strip().split('\n')[0] Msg(' {}{}\n'.format(cls_info[0].upper(),cls_info[1:])) - max_w = max(map(len,bc._user_commands())) - for cmd in bc._user_commands(): + max_w = max(map(len,bc.user_commands)) + for cmd in bc.user_commands: Msg(' {:{w}} {}'.format(cmd,_create_call_sig(cmd),w=max_w)) Msg('') Msg(m2) - elif cmd in MMGenToolCmd._user_commands(): - msg('{}'.format(capfirst(getattr(MMGenToolCmd,cmd).__doc__.strip()))) + elif cmd in MMGenToolCmds: + msg('{}'.format(capfirst(MMGenToolCmds[cmd].__doc__.strip()))) msg('USAGE: {} {} {}'.format(g.prog_name,cmd,_create_call_sig(cmd))) else: die(1,"'{}': no such tool command".format(cmd)) @@ -236,11 +236,43 @@ mnemonic_fmts = { } mn_opts_disp = "(valid options: '{}')".format("', '".join(mnemonic_fmts)) -class MMGenToolCmds(object): +class MMGenToolCmdMeta(type): + classes = {} + methods = {} + def __new__(mcls,name,bases,namespace): + methods = {k:v for k,v in namespace.items() if k[0] != '_' and callable(v)} + if g.test_suite: + if name in mcls.classes: + raise ValueError(f'Class {name!r} already defined!') + for m in methods: + if m in mcls.methods: + raise ValueError(f'Method {m!r} already defined!') + if not getattr(m,'__doc__',None): + raise ValueError(f'Method {m!r} has no doc string!') + cls = super().__new__(mcls,name,bases,namespace) + if bases and name != 'tool_api': + mcls.classes[name] = cls + mcls.methods.update(methods) + return cls - @classmethod - def _user_commands(cls): - return [e for e in dir(cls) if e[0] != '_' and getattr(cls,e).__doc__ and callable(getattr(cls,e))] + def __iter__(cls): + return cls.methods.__iter__() + + def __getitem__(cls,val): + return cls.methods.__getitem__(val) + + def __contains__(cls,val): + return cls.methods.__contains__(val) + + def call(cls,cmd_name,*args,**kwargs): + subcls = cls.classes[cls.methods[cmd_name].__qualname__.split('.')[0]] + return getattr(subcls(),cmd_name)(*args,**kwargs) + + @property + def user_commands(cls): + return {k:v for k,v in cls.__dict__.items() if k in cls.methods} + +class MMGenToolCmds(metaclass=MMGenToolCmdMeta): pass class MMGenToolCmdMisc(MMGenToolCmds): "miscellaneous commands" @@ -1097,19 +1129,6 @@ class MMGenToolCmdMonero(MMGenToolCmds): return True -class MMGenToolCmd( - MMGenToolCmdMisc, - MMGenToolCmdUtil, - MMGenToolCmdCoin, - MMGenToolCmdMnemonic, - MMGenToolCmdFile, - MMGenToolCmdFileCrypt, - MMGenToolCmdFileUtil, - MMGenToolCmdWallet, - MMGenToolCmdRPC, - MMGenToolCmdMonero, - ): pass - class tool_api( MMGenToolCmdUtil, MMGenToolCmdCoin, diff --git a/test/tooltest.py b/test/tooltest.py index 9d0ee8e8..6d11c5a8 100755 --- a/test/tooltest.py +++ b/test/tooltest.py @@ -171,7 +171,7 @@ if opt.list_names: ignore = () from mmgen.tool import MMGenToolCmd uc = sorted( - set(MMGenToolCmd._user_commands()) - + set(MMGenToolCmds) - set(ignore) - set(tested_in['tooltest.py']) - set(tested_in['tooltest2.py']) - diff --git a/test/tooltest2.py b/test/tooltest2.py index f9a3091e..21e069a8 100755 --- a/test/tooltest2.py +++ b/test/tooltest2.py @@ -783,8 +783,7 @@ def run_test(gid,cmd_name): else: if g.coin != 'BTC' or g.testnet: return m2 = '' - m = '{} {}{}'.format(purple('Testing'), cmd_name if opt.names else - docstring_head(getattr(getattr(tool,'MMGenToolCmd'+gid),cmd_name)),m2) + m = '{} {}{}'.format(purple('Testing'), cmd_name if opt.names else docstring_head(tc[cmd_name]),m2) msg_r(green(m)+'\n' if opt.verbose else m) @@ -821,7 +820,7 @@ def run_test(gid,cmd_name): os.close(fd1) stdin_save = os.dup(0) os.dup2(fd0,0) - cmd_out = getattr(tc,cmd_name)(*aargs,**kwargs) + cmd_out = tc.call(cmd_name,*aargs,**kwargs) os.dup2(stdin_save,0) os.wait() opt.quiet = oq_save @@ -832,7 +831,7 @@ def run_test(gid,cmd_name): vmsg('Input: {!r}'.format(stdin_input)) sys.exit(0) else: - ret = getattr(tc,cmd_name)(*aargs,**kwargs) + ret = tc.call(cmd_name,*aargs,**kwargs) opt.quiet = oq_save return ret @@ -917,9 +916,9 @@ def docstring_head(obj): def do_group(gid): qmsg(blue("Testing {}".format( "command group '{}'".format(gid) if opt.names - else docstring_head(getattr(tool,'MMGenToolCmd'+gid))))) + else docstring_head(tc.classes['MMGenToolCmd'+gid])))) - for cname in [e for e in dir(getattr(tool,'MMGenToolCmd'+gid)) if e[0] != '_']: + for cname in tc.classes['MMGenToolCmd'+gid].user_commands: if cname not in tests[gid]: m = 'No test for command {!r} in group {!r}!'.format(cname,gid) if opt.die_on_missing: @@ -950,11 +949,12 @@ if opt.tool_api: del tests['File'] import mmgen.tool as tool +tc = tool.MMGenToolCmds if opt.list_tests: Msg('Available tests:') for gid in tests: - Msg(' {:6} - {}'.format(gid,docstring_head(getattr(tool,'MMGenToolCmd'+gid)))) + Msg(' {:6} - {}'.format(gid,docstring_head(tc.classes['MMGenToolCmd'+gid]))) sys.exit(0) if opt.list_tested_cmds: @@ -986,7 +986,6 @@ if opt.fork: tool_cmd = ('python3',) + tool_cmd else: opt.usr_randchars = 0 - tc = tool.MMGenToolCmd() start_time = int(time.time())