objmethods.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2024 The MMGen Project <mmgen@tuta.io>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. """
  19. objmethods: Mixin classes for MMGen data objects
  20. """
  21. import unicodedata
  22. from . import color as color_mod
  23. if 'MMGenObjectDevTools' in __builtins__: # added to builtins by devinit.init_dev()
  24. MMGenObject = __builtins__['MMGenObjectDevTools']
  25. else:
  26. class MMGenObject:
  27. 'placeholder - overridden when testing'
  28. def immutable_attr_init_check(self):
  29. pass
  30. def truncate_str(s,width): # width = screen width
  31. wide_count = 0
  32. for n,ch in enumerate(s,1):
  33. wide_count += unicodedata.east_asian_width(ch) in ('F','W')
  34. if n + wide_count > width:
  35. return s[:n-1] + ('',' ')[
  36. unicodedata.east_asian_width(ch) in ('F','W')
  37. and n + wide_count == width + 1]
  38. else:
  39. raise ValueError('string requires no truncating')
  40. class Hilite:
  41. color = 'red'
  42. width = 0
  43. trunc_ok = True
  44. # class method equivalent of fmt()
  45. @classmethod
  46. def fmtc( cls, s, width, color=False ):
  47. if len(s) > width:
  48. assert cls.trunc_ok, "If 'trunc_ok' is false, 'width' must be >= width of string"
  49. return cls.colorize( s[:width].ljust(width), color=color )
  50. else:
  51. return cls.colorize( s.ljust(width), color=color )
  52. @classmethod
  53. def hlc(cls,s,color=True):
  54. return getattr( color_mod, cls.color )(s) if color else s
  55. @classmethod
  56. def colorize(cls,s,color=True):
  57. return getattr( color_mod, cls.color )(s) if color else s
  58. @classmethod
  59. def colorize2(cls,s,color=True,color_override=''):
  60. return getattr( color_mod, color_override or cls.color )(s) if color else s
  61. class HiliteStr(str,Hilite):
  62. # supports single-width characters only
  63. def fmt( self, width, color=False ):
  64. if len(self) > width:
  65. assert self.trunc_ok, "If 'trunc_ok' is false, 'width' must be >= width of string"
  66. return self.colorize( self[:width].ljust(width), color=color )
  67. else:
  68. return self.colorize( self.ljust(width), color=color )
  69. # an alternative to fmt(), with double-width char support and other features
  70. def fmt2(
  71. self,
  72. width, # screen width - must be at least 2 (one wide char)
  73. color = False,
  74. encl = '', # if set, must be exactly 2 single-width chars
  75. nullrepl = '',
  76. append_chars = '', # single-width chars only
  77. append_color = False,
  78. color_override = '' ):
  79. if self == '':
  80. return getattr( color_mod, self.color )(nullrepl.ljust(width)) if color else nullrepl.ljust(width)
  81. s_wide_count = len(['' for ch in self if unicodedata.east_asian_width(ch) in ('F','W')])
  82. a,b = encl or ('','')
  83. add_len = len(append_chars) + len(encl)
  84. if len(self) + s_wide_count + add_len > width:
  85. assert self.trunc_ok, "If 'trunc_ok' is false, 'width' must be >= screen width of string"
  86. s = a + (truncate_str(self,width-add_len) if s_wide_count else self[:width-add_len]) + b
  87. else:
  88. s = a + self + b
  89. if append_chars:
  90. return (
  91. self.colorize(s,color=color)
  92. + self.colorize2(
  93. append_chars.ljust(width-len(s)-s_wide_count),
  94. color_override = append_color ))
  95. else:
  96. return self.colorize2( s.ljust(width-s_wide_count), color=color, color_override=color_override )
  97. def hl(self,color=True):
  98. return getattr( color_mod, self.color )(self) if color else self
  99. # an alternative to hl(), with enclosure and color override
  100. # can be called as an unbound method with class as first argument
  101. def hl2(self,s=None,color=True,encl='',color_override=''):
  102. if encl:
  103. return self.colorize2( encl[0]+(s or self)+encl[1], color=color, color_override=color_override )
  104. else:
  105. return self.colorize2( (s or self), color=color, color_override=color_override )
  106. class InitErrors:
  107. @classmethod
  108. def init_fail(cls,e,m,e2=None,m2=None,objname=None,preformat=False):
  109. def get_errmsg():
  110. ret = m if preformat else (
  111. '{!r}: value cannot be converted to {} {}({!s})'.format(
  112. m,
  113. (objname or cls.__name__),
  114. (f'({e2!s}) ' if e2 else ''),
  115. e ))
  116. return f'{m2!r}\n{ret}' if m2 else ret
  117. if hasattr(cls,'passthru_excs') and type(e).__name__ in cls.passthru_excs:
  118. raise e
  119. from .util import die
  120. die(getattr(cls,'exc','ObjectInitError'), get_errmsg())
  121. @classmethod
  122. def method_not_implemented(cls):
  123. import traceback
  124. raise NotImplementedError(
  125. 'method {}() not implemented for class {!r}'.format(
  126. traceback.extract_stack()[-2].name, cls.__name__) )