Browse Source

new ClassFlags,ClassOpts classes

The MMGen Project 3 years ago
parent
commit
f379cde0fc
4 changed files with 169 additions and 0 deletions
  1. 1 0
      mmgen/exception.py
  2. 88 0
      mmgen/flags.py
  3. 1 0
      setup.py
  4. 79 0
      test/unit_tests_d/ut_flags.py

+ 1 - 0
mmgen/exception.py

@@ -48,6 +48,7 @@ class BaseConversionError(Exception):     mmcode = 2
 class BaseConversionPadError(Exception):  mmcode = 2
 class TransactionChainMismatch(Exception):mmcode = 2
 class ObjectInitError(Exception):         mmcode = 2
+class ClassFlagsError(Exception):         mmcode = 2
 
 # 3: yellow hl, 'MMGen Error' + exception + message
 class RPCFailure(Exception):              mmcode = 3

+ 88 - 0
mmgen/flags.py

@@ -0,0 +1,88 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C)2013-2021 The MMGen Project <mmgen@tuta.io>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+"""
+flags.py:  Class flags and opts for the MMGen suite
+"""
+
+from .exception import ClassFlagsError
+from .base_obj import AttrCtrl,Lockable
+from .util import fmt_list
+
+class ClassFlags(AttrCtrl):
+	_name = 'flags'
+	_desc = 'flag'
+	reserved_attrs = ('lock',)
+
+	def __init__(self,parent,arg):
+		self._parent = parent
+		self._available = getattr(self._parent,'avail_'+self._name)
+
+		for a in self._available:
+			if a.startswith('_'):
+				raise ClassFlagsError(f'{a!r}: {self._desc} cannot begin with an underscore')
+			for b in self.reserved_attrs:
+				if a == b:
+					raise ClassFlagsError(f'{a!r}: {b} is a reserved name for {self._desc}')
+
+		if arg:
+			assert type(arg) in (list,tuple), f"{arg!r}: {self._name!r} must be list or tuple"
+		else:
+			arg = []
+
+		for e in arg:
+			if e not in self._available:
+				self.not_available_error(e)
+
+		for e in self._available:
+			setattr(self,e,e in arg)
+
+	def __dir__(self):
+		return [k for k in self.__dict__ if not k.startswith('_') and not k in self.reserved_attrs]
+
+	def __str__(self):
+		return ' '.join(f'{k}={getattr(self,k)}' for k in dir(self))
+
+	def __setattr__(self,name,val):
+
+		if self._lock:
+
+			if name not in self._available:
+				self.not_available_error(name)
+
+			if self._name == 'flags':
+				assert type(val) is bool, f'{val!r} not boolean'
+				old_val = getattr(self,name)
+				if val and old_val:
+					raise ClassFlagsError(f'{self._desc} {name!r} already set')
+				if not val and not old_val:
+					raise ClassFlagsError(f'{self._desc} {name!r} not set, so cannot be unset')
+
+		super().__setattr__(name,val)
+
+	def not_available_error(self,name):
+		raise ClassFlagsError('{!r}: unrecognized {} for {}: (available {}: {})'.format(
+			name,
+			self._desc,
+			type(self._parent).__name__,
+			self._name,
+			fmt_list(self._available,fmt='bare') ))
+
+class ClassOpts(ClassFlags,Lockable):
+	_name = 'opts'
+	_desc = 'opt'

+ 1 - 0
setup.py

@@ -127,6 +127,7 @@ setup(
 			'mmgen.ed25519ll_djbec',
 			'mmgen.exception',
 			'mmgen.filename',
+			'mmgen.flags',
 			'mmgen.globalvars',
 			'mmgen.help',
 			'mmgen.keccak',

+ 79 - 0
test/unit_tests_d/ut_flags.py

@@ -0,0 +1,79 @@
+#!/usr/bin/env python3
+"""
+test/unit_tests_d/ut_flags.py: unit test for the MMGen suite's ClassFlags class
+"""
+
+from mmgen.common import *
+from mmgen.exception import *
+from mmgen.flags import *
+
+class unit_test(object):
+
+	def run_test(self,name,ut):
+
+		class cls1:
+			avail_opts = ()
+			avail_flags = ()
+			def __init__(self,opts=None,flags=None):
+				self.opt = ClassOpts(self,opts)
+				self.flag = ClassFlags(self,flags)
+
+		class cls2(cls1):
+			avail_opts = ('foo','bar')
+			avail_flags = ('baz',)
+
+		class cls3(cls1):
+			avail_opts = ('_foo',)
+
+		class cls4(cls1):
+			avail_opts = ('lock',)
+
+		def test_flags():
+			def gen():
+				for n,cls in enumerate((
+						cls1(),
+						cls2(),
+						cls2(opts=['bar']),
+						cls2(flags=['baz']),
+					)):
+					vmsg(f'Cfg {n+1}:')
+					for k in ('opt','flag'):
+						vmsg('  {}s: {}'.format( k, getattr(cls,k) ))
+					yield cls
+			return list(gen())
+
+		def test_flags_err(d):
+
+			def bad1(): d[1].flag.foo = False
+			def bad2(): d[1].opt.baz = False
+			def bad3(): cls3()
+			def bad4(): cls4()
+			def bad5(): cls1(opts='foo')
+			def bad6(): cls2(opts=['qux'])
+			def bad7(): d[1].flag.baz = False
+			def bad8(): d[3].flag.baz = True
+			def bad9(): d[1].flag.baz = 'x'
+
+			ut.process_bad_data((
+				('flag (1)',             'ClassFlagsError', 'unrecognized flag', bad1 ),
+				('opt (1)',              'ClassFlagsError', 'unrecognized opt',  bad2 ),
+				('avail_opts (1)',       'ClassFlagsError', 'underscore',        bad3 ),
+				('avail_opts (1)',       'ClassFlagsError', 'reserved name',     bad4 ),
+				('class invocation (1)', 'AssertionError',  'list or tuple',     bad5 ),
+				('class invocation (2)', 'ClassFlagsError', 'unrecognized opt',  bad6 ),
+				('flag (2)',             'ClassFlagsError', 'not set',           bad7 ),
+				('flag (3)',             'ClassFlagsError', 'already set',       bad8 ),
+				('flag (4)',             'AssertionError',  'not boolean',       bad9 ),
+			))
+
+		qmsg_r('Testing flags and opts...')
+		vmsg('')
+		classes = test_flags()
+		qmsg('OK')
+
+		qmsg_r('Testing error handling for flags and opts...')
+		vmsg('')
+		test_flags_err(classes)
+		qmsg('OK')
+
+		return True