Browse Source

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
The MMGen Project 5 years ago
parent
commit
068377cf1b
4 changed files with 55 additions and 39 deletions
  1. 5 7
      mmgen/main_tool.py
  2. 42 23
      mmgen/tool.py
  3. 1 1
      test/tooltest.py
  4. 7 8
      test/tooltest2.py

+ 5 - 7
mmgen/main_tool.py

@@ -26,7 +26,7 @@ from mmgen.common import *
 def make_cmd_help():
 def make_cmd_help():
 	import mmgen.tool
 	import mmgen.tool
 	def make_help():
 	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')
 			cls_doc = bc.__doc__.strip().split('\n')
 			for l in cls_doc:
 			for l in cls_doc:
 				if l is cls_doc[0]:
 				if l is cls_doc[0]:
@@ -39,10 +39,9 @@ def make_cmd_help():
 					yield ''
 					yield ''
 			yield ''
 			yield ''
 
 
-			max_w = max(map(len,bc._user_commands()))
+			max_w = max(map(len,bc.user_commands))
 			fs = '  {{:{}}} - {{}}'.format(max_w)
 			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__:
 				if code.__doc__:
 					yield fs.format(name,
 					yield fs.format(name,
 						pretty_format(
 						pretty_format(
@@ -98,16 +97,15 @@ if len(cmd_args) < 1: opts.usage()
 cmd = cmd_args.pop(0)
 cmd = cmd_args.pop(0)
 
 
 import mmgen.tool as tool
 import mmgen.tool as tool
-tc = tool.MMGenToolCmd()
 
 
 if cmd in ('help','usage') and cmd_args:
 if cmd in ('help','usage') and cmd_args:
 	cmd_args[0] = 'command_name=' + cmd_args[0]
 	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))
 	die(1,"'{}': no such command".format(cmd))
 
 
 args,kwargs = tool._process_args(cmd,cmd_args)
 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)
 tool._process_result(ret,pager='pager' in kwargs and kwargs['pager'],print_result=True)

+ 42 - 23
mmgen/tool.py

@@ -32,7 +32,7 @@ def _options_annot_str(l):
 
 
 def _create_call_sig(cmd,parsed=False):
 def _create_call_sig(cmd,parsed=False):
 
 
-	m = getattr(MMGenToolCmd,cmd)
+	m = MMGenToolCmds[cmd]
 
 
 	if 'varargs_call_sig' in m.__code__.co_varnames: # hack
 	if 'varargs_call_sig' in m.__code__.co_varnames: # hack
 		flag = 'VAR_ARGS'
 		flag = 'VAR_ARGS'
@@ -90,16 +90,16 @@ def _usage(cmd=None,exit_val=1):
 
 
 	if not cmd:
 	if not cmd:
 		Msg(m1)
 		Msg(m1)
-		for bc in MMGenToolCmd.__bases__:
+		for bc in MMGenToolCmds.classes.values():
 			cls_info = bc.__doc__.strip().split('\n')[0]
 			cls_info = bc.__doc__.strip().split('\n')[0]
 			Msg('  {}{}\n'.format(cls_info[0].upper(),cls_info[1:]))
 			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('    {:{w}} {}'.format(cmd,_create_call_sig(cmd),w=max_w))
 			Msg('')
 			Msg('')
 		Msg(m2)
 		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)))
 		msg('USAGE: {} {} {}'.format(g.prog_name,cmd,_create_call_sig(cmd)))
 	else:
 	else:
 		die(1,"'{}': no such tool command".format(cmd))
 		die(1,"'{}': no such tool command".format(cmd))
@@ -236,11 +236,43 @@ mnemonic_fmts = {
 }
 }
 mn_opts_disp = "(valid options: '{}')".format("', '".join(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
+
+	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)
 
 
-	@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))]
+	@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):
 class MMGenToolCmdMisc(MMGenToolCmds):
 	"miscellaneous commands"
 	"miscellaneous commands"
@@ -1097,19 +1129,6 @@ class MMGenToolCmdMonero(MMGenToolCmds):
 
 
 		return True
 		return True
 
 
-class MMGenToolCmd(
-		MMGenToolCmdMisc,
-		MMGenToolCmdUtil,
-		MMGenToolCmdCoin,
-		MMGenToolCmdMnemonic,
-		MMGenToolCmdFile,
-		MMGenToolCmdFileCrypt,
-		MMGenToolCmdFileUtil,
-		MMGenToolCmdWallet,
-		MMGenToolCmdRPC,
-		MMGenToolCmdMonero,
-	): pass
-
 class tool_api(
 class tool_api(
 		MMGenToolCmdUtil,
 		MMGenToolCmdUtil,
 		MMGenToolCmdCoin,
 		MMGenToolCmdCoin,

+ 1 - 1
test/tooltest.py

@@ -171,7 +171,7 @@ if opt.list_names:
 	ignore = ()
 	ignore = ()
 	from mmgen.tool import MMGenToolCmd
 	from mmgen.tool import MMGenToolCmd
 	uc = sorted(
 	uc = sorted(
-		set(MMGenToolCmd._user_commands()) -
+		set(MMGenToolCmds) -
 		set(ignore) -
 		set(ignore) -
 		set(tested_in['tooltest.py']) -
 		set(tested_in['tooltest.py']) -
 		set(tested_in['tooltest2.py']) -
 		set(tested_in['tooltest2.py']) -

+ 7 - 8
test/tooltest2.py

@@ -783,8 +783,7 @@ def run_test(gid,cmd_name):
 	else:
 	else:
 		if g.coin != 'BTC' or g.testnet: return
 		if g.coin != 'BTC' or g.testnet: return
 		m2 = ''
 		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)
 	msg_r(green(m)+'\n' if opt.verbose else m)
 
 
@@ -821,7 +820,7 @@ def run_test(gid,cmd_name):
 				os.close(fd1)
 				os.close(fd1)
 				stdin_save = os.dup(0)
 				stdin_save = os.dup(0)
 				os.dup2(fd0,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.dup2(stdin_save,0)
 				os.wait()
 				os.wait()
 				opt.quiet = oq_save
 				opt.quiet = oq_save
@@ -832,7 +831,7 @@ def run_test(gid,cmd_name):
 				vmsg('Input: {!r}'.format(stdin_input))
 				vmsg('Input: {!r}'.format(stdin_input))
 				sys.exit(0)
 				sys.exit(0)
 		else:
 		else:
-			ret = getattr(tc,cmd_name)(*aargs,**kwargs)
+			ret = tc.call(cmd_name,*aargs,**kwargs)
 			opt.quiet = oq_save
 			opt.quiet = oq_save
 			return ret
 			return ret
 
 
@@ -917,9 +916,9 @@ def docstring_head(obj):
 def do_group(gid):
 def do_group(gid):
 	qmsg(blue("Testing {}".format(
 	qmsg(blue("Testing {}".format(
 		"command group '{}'".format(gid) if opt.names
 		"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]:
 		if cname not in tests[gid]:
 			m = 'No test for command {!r} in group {!r}!'.format(cname,gid)
 			m = 'No test for command {!r} in group {!r}!'.format(cname,gid)
 			if opt.die_on_missing:
 			if opt.die_on_missing:
@@ -950,11 +949,12 @@ if opt.tool_api:
 	del tests['File']
 	del tests['File']
 
 
 import mmgen.tool as tool
 import mmgen.tool as tool
+tc = tool.MMGenToolCmds
 
 
 if opt.list_tests:
 if opt.list_tests:
 	Msg('Available tests:')
 	Msg('Available tests:')
 	for gid in 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)
 	sys.exit(0)
 
 
 if opt.list_tested_cmds:
 if opt.list_tested_cmds:
@@ -986,7 +986,6 @@ if opt.fork:
 		tool_cmd = ('python3',) + tool_cmd
 		tool_cmd = ('python3',) + tool_cmd
 else:
 else:
 	opt.usr_randchars = 0
 	opt.usr_randchars = 0
-	tc = tool.MMGenToolCmd()
 
 
 start_time = int(time.time())
 start_time = int(time.time())