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
This commit is contained in:
The MMGen Project 2020-04-08 08:51:19 +00:00
commit 068377cf1b
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
4 changed files with 55 additions and 39 deletions

View file

@ -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)

View file

@ -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,

View file

@ -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']) -

View file

@ -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())