# -*- coding: UTF-8 -*- #/** # * Software Name : pycrate # * Version : 0.4 # * # * Copyright 2016. Benoit Michau. ANSSI. # * Copyright 2019. Benoit Michau. P1Sec. # * # * This library is free software; you can redistribute it and/or # * modify it under the terms of the GNU Lesser General Public # * License as published by the Free Software Foundation; either # * version 2.1 of the License, or (at your option) any later version. # * # * This library 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 # * Lesser General Public License for more details. # * # * You should have received a copy of the GNU Lesser General Public # * License along with this library; if not, write to the Free Software # * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # * MA 02110-1301 USA # * # *-------------------------------------------------------- # * File Name : pycrate_core/elt.py # * Created : 2016-02-20 # * Authors : Benoit Michau # *-------------------------------------------------------- #*/ __all__ = ['EltErr', 'REPR_RAW', 'REPR_HEX', 'REPR_BIN', 'REPR_HD', 'REPR_HUM', 'Element', 'Atom', 'Envelope', 'Array', 'Sequence', 'Alt'] from binascii import hexlify try: from json import JSONEncoder, JSONDecoder except ImportError: _with_json = False else: _with_json = True JsonEnc = JSONEncoder(sort_keys=True, indent=1) JsonDec = JSONDecoder() try: import JSONDecodeError except ImportError: # it seems Python2.7 removed support for JSONDecodeError at some point JSONDecodeError = ValueError from .utils import * from .charpy import Charpy, CharpyErr #------------------------------------------------------------------------------# # Elt specific error #------------------------------------------------------------------------------# class EltErr(PycrateErr): pass #------------------------------------------------------------------------------# # global values for Element representation #------------------------------------------------------------------------------# REPR_RAW = 0 REPR_HEX = 1 REPR_BIN = 2 REPR_HD = 3 REPR_HUM = 4 # for hexdump representation if python_version < 3: def hview(buf, lw=16): hv = [] for o in range(0, len(buf), lw): l = buf[o:o+lw] # create the hex fmt string for each iteration hs = '%.2x ' * len(l) % tuple(map(ord, l)) hv.append( ' ' + hs + ' '*(3*lw-len(hs)) + '| %r' % l ) return hv else: def hview(buf, lw=16): hv = [] for o in range(0, len(buf), lw): l = buf[o:o+lw] # create the hex fmt string for each iteration hs = '%.2x ' * len(l) % tuple(l) hv.append( ' ' + hs + ' '*(3*lw-len(hs)) + '| %r' % l ) return hv #------------------------------------------------------------------------------# # Element parent class #------------------------------------------------------------------------------# ### class attributes to be inherited from Element: #_SAFE_STAT #_SAFE_DYN ### methods to be inherited from Element #_log() ## envelope, hierarchy, selection routines #set_env() #get_env() #get_next() #get_prev() #set_hier() #inc_hier() #dec_hier() #get_hier() #get_hier_abs() #get_header() #get_payload() ## format routines #get_len() #set_trans() #set_transauto() #get_trans() ## conversion routines #from_bytes() #to_bytes() #__str__() (py2) / __bytes__() (py3) #from_uint() #to_uint() #from_int() #to_int() ## representation routines #bin() #__bin__() #hex() #__hex__() class Element(object): """ Parent class for all atomic (Atom and children from base.py) and composite (Envelope, Array, Sequence, Alt) elements """ # safety checks against user-provided data # when setting static values _SAFE_STAT = True # when computing automatic values _SAFE_DYN = True # next / prev / header / payload element selection within an envelope # select or not transparent element ENV_SEL_TRANS = True # hardcoded class name CLASS = 'Element' # default transparency DEFAULT_TRANS = False # default attributes value _env = None _hier = 0 _trans = None _transauto = None def _log(self, msg=''): log('[%s] %s' % self._name, msg) #--------------------------------------------------------------------------# # envelope, hierarchy and selection routines #--------------------------------------------------------------------------# def set_env(self, env): """Set the envelope around self Args: env (element) : direct envelope of the element Returns: None Raises: EltErr : if self._SAFE_STAT enabled and env is not a valid envelope """ if env is None: try: del self._env except Exception: pass else: if self._SAFE_STAT and not isinstance(env, (Envelope, Array, Sequence, Alt)): raise(EltErr('{0} [set_env]: env type is {1}, expecting None, '\ 'Envelope, Sequence, Array or Alt'\ .format(self._name, type(self._env).__name__))) self._env = env def get_env(self): """Returns the envelope around self Args: None Returns: env (element) : first envelope around element or None """ return self._env def get_next(self, val=1): """Returns the next element in the envelope around self Args: val (int) : number of elements to go to after self Returns: next (element) : next element selected within the envelope Raises: EltErr : if self._SAFE_STAT enabled and val overflows the number of elements within the envelope """ if self._env is None: return None # get index of self within its envelope try: ind = self._env.index(self) except Exception: return None try: if self.ENV_SEL_TRANS: return self._env[ind+val] else: # do not count transparent element within the envelope i = 1 while i <= val: if not self._env[ind+i].get_trans(): i += 1 return self._env[ind+i] except EltErr: #raise(EltErr('{0} [get_next]: invalid index {1} within envelope {2}'\ # .format(self._name, ind+val, self._env._name))) return None def get_prev(self, val=1): """Returns the previous element in the envelope around self Args: val (int) : number of elements to go to before self Returns: next (element) : previous element selected within the envelope Raises: EltErr : if self._SAFE_STAT enabled and val overflows the number of elements within the envelope """ if self._env is None: return None # get index of self within its envelope try: ind = self._env.index(self) except Exception: return None if ind-val < 0: return None try: if self.ENV_SEL_TRANS: return self._env[ind-val] else: # do not count transparent element within the envelope i = 1 while i <= val: if not self._env[ind-i].get_trans(): i += 1 return self._env[ind-i] except EltErr: #raise(EltErr('{0} [get_prev]: invalid index {1} within envelope {2}'\ # .format(self._name, ind-val, self._env._name))) return None def set_hier(self, hier): """Set the hierarchical level of self, relative to the one of the envelope around it if exists, absolute otherwise Args: hier (int) : hierarchical level of the element Returns: None Raises: EltErr : if self._SAFE_STAT enabled and hier is not an unsigned integer """ if self._SAFE_STAT: self._chk_hier(hier) self._hier = hier def _chk_hier(self, *args): if args: hier = args[0] else: hier = self._hier if not isinstance(hier, integer_types): raise(EltErr('{0} [_chk_hier]: hier type is {1}, expecting integer'\ .format(self._name, type(self._hier).__name__))) elif hier < 0: raise(EltErr('{0} [_chk_hier]: hier value is {1}, expecting unsigned value'\ .format(self._name, self._hier))) def inc_hier(self, hier=1): """Increment the hierarchical level of self, relative to the one of the envelope if exists, absolute otherwise Args: hier (int) : value to add to the hierarchical level Returns: None Raises: EltErr : if self._SAFE_STAT enabled and resulting hier is not an unsigned integer """ if self._SAFE_STAT: self._chk_hier(self._hier + hier) self._hier += hier def dec_hier(self, hier=1): """Decrement the hierarchical level of self, relative to the one of the envelope if exists, absolute otherwise Args: hier (int) : value to substract to the hierarchical level Returns: None Raises: EltErr : if self._SAFE_STAT enabled and resulting hier is not an unsigned integer """ if self._SAFE_STAT: self._chk_hier(self._hier - hier) self._hier -= hier def get_hier(self): """Returns the hierarchical level of self within its envelope if exists, absolute otherwise Args: None Returns: hier (int) : hierarchical level of self, unsigned """ return self._hier def get_hier_abs(self): """Returns the absolute hierarchical level of self Args: None Returns: hier (int) : hierarchical level of the element, including the hierarchy of all envelopes around it """ hier = self._hier env = self._env while env is not None: hier += env._hier env = env._env return hier def get_header(self): """Returns the header of self, according to their hierarchical level. The header is the 1st element before self with a lower hierarchy Args: None Returns: hdr (element) : header of self, or None if no header is found """ # go over all order of envelopes if necessary to find the 1st element # with a lower hierarchical level than the element's one elt = self hier = self._hier env = self._env # if self.ENV_SEL_TRANS: while env is not None: try: ind = env.index(elt) except Exception: return None for elt in env[ind-1::-1]: if elt._hier < hier: return elt elt = env hier = elt._hier env = elt._env else: while env is not None: try: ind = env.index(elt) except Exception: return None for elt in env[ind-1::-1]: if not elt.get_trans() and elt._hier < hier: return elt elt = env hier = elt._hier env = elt._env # return None def get_payload(self): """Returns a (sliced) envelope with all the payload elements of self, according to their hierarchical level. The payload is the list of all elements after self with a higher hierarchy, without an element with a lower hierarchy than self in between Args: None Returns: pay (element) : envelope with all payload elements of self, or None if no payload is found """ # go over all order of envelopes if necessary to find the 1st element # with a higher hierarchical level than the element's one elt = self hier = self._hier env = self._env # if self.ENV_SEL_TRANS: while env is not None: try: ind_start = env.index(elt) except Exception: return None ind = 1+ind_start ind_pay = [None, None] for elt in env[1+ind_start:]: # get the window of indexes corresponding to # the full payload if elt._hier > hier and ind_pay[0] is None: ind_pay[0] = ind elif elt._hier < hier and ind_pay[0] is not None: ind_pay[1] = ind return env[ind_pay[0]:ind_pay[1]] ind += 1 if ind_pay[0] is not None: return env[ind_pay[0]:] else: elt = env hier = elt._hier env = elt._env else: while env is not None: try: ind_start = env.index(elt) except Exception: return None ind = 1+ind_start ind_pay = [None, None] for elt in env[1+ind_start:]: # get the window of indexes corresponding to # the full payload, jumping over transparent element if not elt.get_trans(): if elt._hier > hier and ind_pay[0] is None: ind_pay[0] = ind elif elt._hier < hier and ind_pay[0] is not None: ind_pay[1] = ind return env[ind_pay[0]:ind_pay[1]] ind += 1 if ind_pay[0] is not None: return env[ind_pay[0]:] else: elt = env hier = elt._hier env = elt._env # return None #--------------------------------------------------------------------------# # format routines #--------------------------------------------------------------------------# def get_len(self): """Returns the length in bytes of self Args: None Returns: bytelen (int) : length in bytes computed """ bl = self.get_bl() if bl%8: return 1 + bl>>3 else: return bl>>3 def set_trans(self, trans=None): """Set the raw transparency of self Args: trans (bool) : raw transparency, default to None Returns: None Raises: EltErr : if self._SAFE_STAT is enabled and trans is not bool """ if trans is None: try: del self._trans except Exception: pass else: if self._SAFE_STAT: self._chk_trans(trans) self._trans = trans def _chk_trans(self, *args): if args: trans = args[0] else: trans = self._trans if not isinstance(trans, (NoneType, bool)): raise(EltErr('{0} [_chk_trans]: trans type is {1}, expecting bool'\ .format(self._name, type(trans).__name__))) def set_transauto(self, transauto=None): """Set the transparency automation of self Args: transauto (callable) : automate the transparency computation, call transauto() to compute trans, default to None Returns: None Raises: EltErr : if self._SAFE_STAT is enabled and transauto is not callable """ if transauto is None: try: del self._transauto except Exception: pass else: if self._SAFE_STAT and not callable(transauto): raise(EltErr('{0} [set_transauto]: transauto type is {1}, expecting callable'\ .format(self._name, type(transauto).__name__))) self._transauto = transauto def get_trans(self): """Returns the transparency of self Args: None Returns: trans (bool) : transparency computed, default to class attribute DEFAULT_TRANS Raises: EltErr : if self._SAFE_DYN is enabled and the value produced dynamically is not bool """ # follow the value resolution order: # 1) raw trans if self._trans is not None: return self._trans # 2) trans automation elif self._transauto is not None: trans = self._transauto() if self._SAFE_DYN: self._chk_trans(trans) return trans # # 3) default transparency else: return self.DEFAULT_TRANS def reautomate(self): """Reset all attributes of the element which have an automation Args: None Returns: None """ if self._transauto is not None and self._trans is not None: del self._trans #--------------------------------------------------------------------------# # conversion routines #--------------------------------------------------------------------------# def from_bytes(self, char): """Consume a bytes buffer or Charpy instance `char' and sets the internal value according to it Args: char (bytes or charpy): bytes buffer or charpy instance to be consumed Returns: None Raises: EltErr : if `char' has not the correct type CharpyErr """.format(self.__class__.__name__) if isinstance(char, bytes_types): char = Charpy(char) elif self._SAFE_STAT and not isinstance(char, Charpy): raise(EltErr('{0} [from_bytes]: char type is {1}, expecting Charpy'\ .format(self._name, type(char).__name__))) # self._from_char(char) def to_bytes(self): """Produce a bytes buffer from the internal value Args: None Returns: char (bytes) : resulting bytes buffer """.format(self.__class__.__name__) return pack_val(*self._to_pack())[0] def from_uint(self, uint, bl=None): """Consume an unsigned integer or Charpy instance `uint' and sets the internal value according to it Args: uint (int or charpy): unsigned integer or Charpy instance to be consumed bl: length in bits for `uint', if None, the minimum number of bits is used Returns: None Raises: EltErr : if `uint' has not the correct type CharpyErr """ if isinstance(uint, integer_types): char = Charpy() char.set_uint(uint, bl) uint = char elif self._SAFE_STAT and not isinstance(uint, Charpy): raise(EltErr('{0} [from_uint]: uint type is {1}, expecting Charpy'\ .format(self._name, type(uint).__name__))) # self._from_char(uint) def to_uint(self): """Produce an unsigned integer from the internal value Args: None Returns: uint (int) : unsigned integer """ try: return bytes_to_uint(self.to_bytes(), self.get_bl()) except PycrateErr: # an invalid value has been set, _SAFE_STAT / DYN is probably disabled # for e.g. fuzzing purpose, but there is still need to not break here b = self.to_bytes() return bytes_to_uint(b, len(b)<<3) def from_int(self, integ, bl=None): """Consume a signed integer or charpy instance `integ' and sets the internal value according to it Args: integ (int or charpy): signed integer or Charpy instance to be consumed bl: length in bits for `integ', if None, the minimum number of bits is used Returns: None Raises: EltErr : if `integ' has not the correct type CharpyErr """ if isinstance(uint, integer_types): char = Charpy() char.set_int(integ, bl) integ = char elif self._SAFE_STAT and not isinstance(integ, Charpy): raise(EltErr('{0} [from_int]: integ type is {1}, expecting Charpy'\ .format(self._name, type(integ).__name__))) # self._from_char(integ) def to_int(self): """Produce a signed integer from the internal value Args: None Returns: integ (int) : signed integer """ try: return bytes_to_int(self.to_bytes(), self.get_bl()) except PycrateErr: # an invalid value has been set, _SAFE_STAT / DYN is probably disabled # for e.g. fuzzing purpose, but there is still need to not break here b = self.to_bytes() return bytes_to_int(b, len(b)<<3) if python_version < 3: __str__ = to_bytes else: __bytes__ = to_bytes #--------------------------------------------------------------------------# # representation routines #--------------------------------------------------------------------------# def bin(self): bl = self.get_bl() if bl == 0: return '' else: bs = bytes_to_bitstr(self.to_bytes()) if len(bs) > bl: return bs[:bl] else: return bs def hex(self): bl = self.get_bl() if bl == 0: return '' else: try: return uint_to_hex(bytes_to_uint(self.to_bytes(), bl), bl) except PycrateErr: # an invalid value has been set, _SAFE_STAT / DYN is probably disabled # for e.g. fuzzing purpose, but there is still need to not break here return hexlify(self.to_bytes()).decode('ascii') __bin__ = bin __hex__ = hex #--------------------------------------------------------------------------# # json api #--------------------------------------------------------------------------# if _with_json: def _from_jval(self, val): raise(EltErr('not impemented')) def _from_jval_wrap(self, val): try: val = val[self._name] except Exception: raise(EltErr('{0} [_from_jval]: invalid value, {1!r}'.format(self._name, val))) else: self._from_jval(val) def from_json(self, txt): if self.get_trans(): return else: try: val = JsonDec.decode(txt) except JSONDecodeError: raise(EltErr('{0} [from_json]: invalid format, {1!r}'.format(self._name, txt))) else: self._from_jval_wrap(val) def _to_jval(self): raise(EltErr('not implemented')) def _to_jval_wrap(self): return {self._name: self._to_jval()} def to_json(self): if self.get_trans(): return '' else: return JsonEnc.encode(self._to_jval_wrap()) #------------------------------------------------------------------------------# # Atom class, for base elements #------------------------------------------------------------------------------# class Atom(Element): """ Parent class for all atomic elements universal attributes: - name: str, custom one or class.__name__ - desc: str, more descriptive text, used for representation - rep: int, type of representation (raw, hex, bin) - val: type depends on Element subclasses, raw value of the atom - bl: int, length of the atom in bits - trans: bool, transparency of the atom - dic: dict, for extended representation of the value - hier: hierarchical level when placed within an envelope automation attributes: - valauto: callable, to automate the production of the atom's raw value - blauto: callable, to automate the length in bits of the atom - transauto: callable, to automate the determination of element's transparency - dicauto: callable, to automate the production of element's dictionnary contextual attributes: - env: envelope, container of the atom """ # hardcoded class name CLASS = 'Atom' # tuple of types accepted as input value and used as possible returned value # WNG: always ensure to flatten the list of types (e.g. when using # bytes_types or integer_types) TYPES = flatten(NoneType, ) TYPENAMES = get_typenames(*TYPES) # representation parameters # type of representation REPR_TYPES = (REPR_RAW, REPR_HEX, REPR_BIN, REPR_HD, REPR_HUM) # if > 0, object representation will be truncated at REPR_MAXLEN REPR_MAXLEN = 0 # default value / bl / trans / dic DEFAULT_VAL = None DEFAULT_BL = 0 DEFAULT_TRANS = False DEFAULT_DIC = {} # default attributes value _env = None _hier = 0 _desc = '' _rep = REPR_RAW _bl = None _blauto = None _val = None _valauto = None _trans = None _transauto = None _dic = None _dicauto = None __attrs__ = ('_env', '_name', '_desc', '_rep', '_hier', '_bl', '_blauto', '_val', '_valauto', '_trans', '_transauto', '_dic', '_dicauto') def __init__(self, *args, **kw): """Initializes an instance of Atom Args: *args: nothing or atom name (str) **kw: name (str): atom name if no args desc (str): additional atom description rep (int in cls.REPR_TYPES): atom representation type hier (int): atom hierarchy level bl (int): atom length in bits val (see self.TYPES): atom value trans (bool): atom transparency dic (dict): atom dictionnary for friendly representation """ # element name in kw, or first args if len(args): self._name = str(args[0]) elif 'name' in kw: self._name = str(kw['name']) # if not provided, it's the class name elif not hasattr(self, '_name'): self._name = self.__class__.__name__ # element description customization if 'desc' in kw: self._desc = str(kw['desc']) # element representation customization if 'rep' in kw and kw['rep'] in self.REPR_TYPES: self._rep = kw['rep'] # element hierarchy if 'hier' in kw: self._hier = kw['hier'] # element bit length if 'bl' in kw: self._bl = kw['bl'] # element value if 'val' in kw: self._val = kw['val'] # element transparency if 'trans' in kw: self._trans = kw['trans'] # element dictionnary if 'dic' in kw: self._dic = kw['dic'] if self._SAFE_STAT: self._chk_hier() self._chk_bl() self._chk_val() self._chk_trans() self._chk_dic() #--------------------------------------------------------------------------# # format routines #--------------------------------------------------------------------------# def set_val(self, val=None): """Set the raw value of self Args: val (see self.TYPES) : raw value, default to None Returns: None Raises: EltErr : if self._SAFE_STAT enabled and val does not have the correct type """ if val is None: try: del self._val except Exception: pass else: if self._SAFE_STAT: self._chk_val(val) self._val = val def _chk_val(self, *args): if args: val = args[0] else: val = self._val if not isinstance(val, self.TYPES + (NoneType,) ): raise(EltErr('{0} [_chk_val]: val type is {1}, expecting {2}'\ .format(self._name, type(val).__name__, self.TYPENAMES))) def set_valauto(self, valauto=None): """Set the value automation callable for self Args: valauto (callable) : automate the value computation, call valauto() to compute the value, default to None Returns: None Raises: EltErr : if self._SAFE_STAT enabled and valauto is not a callable """ if valauto is None: try: del self._valauto except Exception: pass else: if self._SAFE_STAT and not callable(valauto): raise(EltErr('{0} [set_valauto]: valauto type is {1}, expecting callable'\ .format(self._name, type(valauto).__name__))) self._valauto = valauto def get_val(self): """Returns the value of self Args: None Returns: value (see self.TYPES) : value computed, default to class attribute DEFAULT_VAL Raises: EltErr : if self._SAFE_DYN is enabled and the value produced dynamically has not the correct type """ # follow the value resolution order: # 1) raw value if self._val is not None: return self._val # 2) value automation elif self._valauto is not None: val = self._valauto() if self._SAFE_DYN: self._chk_val(val) return val # 3) default value else: return self.DEFAULT_VAL # for atomic element, no dict to be returned, but just the standard value get_val_d = get_val def set_bl(self, bl=None): """Set the raw length in bits of self Args: bl (int) : raw bit length, default to None Returns: None Raises: EltErr : if self._SAFE_STAT is enabled and bl is not integer """ if bl is None: try: del self._bl except Exception: pass else: if self._SAFE_STAT: self._chk_bl(bl) self._bl = bl def _chk_bl(self, *args): if args: bl = args[0] else: bl = self._bl if not isinstance(bl, integer_types + (NoneType,)): raise(EltErr('{0} [_chk_bl]: bl type is {1}, expecting integer'\ .format(self._name, type(bl).__name__))) def set_blauto(self, blauto=None): """Set an automation for the length in bits of self, used only when mapping an external buffer to it. If bl is None, self._get_bl_from_val() is used Args: blauto (callable) : automate the bl computation, call blauto() to compute the bit length, default to None Returns: None Raises: EltErr : if self._SAFE_STAT is enabled and blauto is not a callable """ if blauto is None: try: del self._blauto except Exception: pass else: if self._SAFE_STAT and not callable(blauto): raise(EltErr('{0} [set_blauto]: blauto type is {1}, expecting callable'\ .format(self._name, type(blauto).__name__))) self._blauto = blauto def set_len(self, l=None): """Set the raw length in bytes of self Args: l (int) : raw byte length, default to None Returns: None Raises: EltErr : if self._SAFE_STAT is enabled and l is not integer """ if l is None: try: del self._bl except Exception: pass else: if self._SAFE_STAT: # no need to multiply for this chk() self._chk_bl(l) self._bl = 8*l def _get_bl_from_val(self): # when bl is not defined at all, it is computed from the value set return 0 def get_bl(self): """Returns the length in bits of self Args: None Returns: bl (int) : length in bits computed default to class attribute DEFAULT_BL """ # follow the value resolution order: # 0) transparency if self.get_trans(): return 0 # 1) raw bl elif self._bl is not None: return self._bl # 2) bl automation: only when parsing buffers # see _from_char() # 3) no bl defined, return the bl computed from the value set elif self._val is not None or self._valauto is not None: return self._get_bl_from_val() # 4) no bl defined, no value defines, return the default one else: return self.DEFAULT_BL def set_dic(self, dic=None): """Set a dictionnary for the value interpretation of self, used for object representation Args: dic (dict) : value / interpretation lookup dict, default to None Returns: None Raises: EltErr : if self._SAFE_STAT is enabled and dic is not dict """ if dic is None: try: del self._dic except Exception: pass else: if self._SAFE_STAT: self._chk_dic(dic) self._dic = dic def _chk_dic(self, *args): if args: dic = args[0] else: dic = self._dic if not isinstance(dic, (NoneType, dict)): raise(EltErr('{0} [_chk_dic]: dic type is {1}, expecting dict'\ .format(self._name, type(dic).__name__))) def set_dicauto(self, dicauto=None): """Set an automation for producing the dictionnary which helps in the value interpretation of self Args: dicauto (callable) : automate the dic computation, call dicauto() to compute the dictionnary, default to None Returns: None Raises: EltErr : if self._SAFE_STAT is enabled and dicauto is not callable """ if dicauto is None: try: del self._dicauto except Exception: pass else: if self._SAFE_STAT and not callable(dicauto): raise(EltErr('{0} [set_dicauto]: dicauto type is {1}, expecting callable'\ .format(self._name, type(dicauto).__name__))) self._dicauto = dicauto def get_dic(self): """Returns the dictionnary for the value interpretation of self Args: None Returns: dic (dict) : dictionnary computed default to class attribute DEFAULT_DIC Raises: EltErr : if self._SAFE_DYN is enabled and the dic produced dynamically is not dict """ # follow the value resolution order: # 1) raw dic if self._dic is not None: return self._dic # 2) dic automation if self._dicauto is not None: dic = self._dicauto() if self._SAFE_DYN: self._chk_dic(dic) return dic # # 3) default dic return self.DEFAULT_DIC def get_val_dic(self): """Returns the looked-up value through the dictionnary returned by self.get_dic() Args: None Returns: dic_val (depends of self.get_dic()) Raises: EltErr : if self._SAFE_DYN is enabled and the dic produced dynamically is not dict """ if self._dic is None and self._dicauto is None: return self._val else: return self.get_dic().get(self._val, self._val) def reautomate(self): """Reset all attributes of self which have an automation Args: None Returns: None """ # restore class attributes if self._valauto is not None and self._val is not None: del self._val if self._blauto is not None and self._bl is not None: del self._bl if self._transauto is not None and self._trans is not None: del self._trans if self._dicauto is not None and self._dic is not None: del self._dic #--------------------------------------------------------------------------# # conversion routines #--------------------------------------------------------------------------# def _to_pack(self): """Produces a tuple ready to be packed with pack_val() according to its internal value """ if not self.get_trans(): return [(TYPE_BYTES, b'', 0)] else: return [] def _from_char(self, char): """Consume the charpy intance and set its internal value according to it """ if not self.get_trans(): self.set_val(None) #--------------------------------------------------------------------------# # copy / cloning routines #--------------------------------------------------------------------------# def get_attrs(self): """Returns the dictionnary of universal attributes of self Args: None Returns: attrs (dict) : dictionnary of attributes """ return {'name' : self._name, 'desc' : self._desc, 'rep' : self._rep, 'hier' : self._hier, 'bl' : self._bl, 'val' : self._val, 'trans': self._trans, 'dic' : self._dic} def get_attrs_all(self): """Returns the dictionnary of all attributes of self Args: None Returns: attrs (dict) : dictionnary of attributes """ return {'env' : self._env, 'name' : self._name, 'desc' : self._desc, 'rep' : self._rep, 'hier' : self._hier, 'bl' : self._bl, 'blauto' : self._blauto, 'val' : self._val, 'valauto' : self._valauto, 'trans' : self._trans, 'transauto': self._transauto, 'dic' : self._dic, 'dicauto' : self._dicauto} def set_attrs(self, **kw): """Updates the attributes of self Args: kw (dict): dict of attributes and associated values attributes can be name, desc, rep, hier, bl, val, trans and dic Returns: None """ if 'name' in kw and isinstance(kw['name'], str): self._name = kw['name'] if 'desc' in kw and isinstance(kw['desc'], str) and kw['desc'] != self.__class__._desc: self._desc = str(kw['desc']) if 'rep' in kw and kw['rep'] in self.REPR_TYPES and \ kw['rep'] != self.__class__._rep: self._rep = kw['rep'] if 'hier' in kw and kw['hier'] != self.__class__._hier: self._hier = kw['hier'] if 'bl' in kw and kw['bl'] != self.__class__._bl: self._bl = kw['bl'] if 'val' in kw and kw['val'] != self.__class__._val: self._val = kw['val'] if 'trans' in kw and kw['trans'] != self.__class__._trans: self._trans = kw['trans'] if 'dic' in kw and kw['dic'] != self.__class__._dic: self._dic = kw['dic'] # if self._SAFE_STAT: self._chk_hier() self._chk_bl() self._chk_val() self._chk_trans() self._chk_dic() def clone(self): """Produces an independent clone of self Args: None Returns: clone (self.__class__ instance) """ kw = {'rep': self._rep} if self._desc != self.__class__._desc: kw['desc'] = self._desc if self._hier != self.__class__._hier: kw['hier'] = self._hier if self._bl != self.__class__._bl: kw['bl'] = self._bl if self._val != self.__class__._val: kw['val'] = self._val if self._trans != self.__class__._trans: kw['trans'] = self._trans if self._dic != self.__class__._dic: kw['dic'] = self._dic return self.__class__(self._name, **kw) #--------------------------------------------------------------------------# # representation routines #--------------------------------------------------------------------------# def repr(self): # element transparency if self.get_trans(): trans = ' [transparent]' else: trans = '' # additional description if self._desc: desc = ' [%s]' % self._desc else: desc = '' # type of representation to be used val = self() if self._rep in (REPR_RAW, REPR_HUM): val_repr = repr(val) elif self._rep == REPR_BIN: val_repr = '0b' + self.bin() elif self._rep in (REPR_HEX, REPR_HD): val_repr = '0x' + self.hex() if self.REPR_MAXLEN > 0 and len(val_repr) > self.REPR_MAXLEN: val_repr = val_repr[:self.REPR_MAXLEN] + '...' # value informative addition with dict dic = self.get_dic() if dic and val in dic: val_inf = ' (%s)' % dic[val] else: val_inf = '' return '<%s%s%s : %s%s>' % (self._name, desc, trans, val_repr, val_inf) def _repr_hd(self): # element transparency if self.get_trans(): trans = ' [transparent]' else: trans = '' # additional description if self._desc: desc = ' [%s]' % self._desc else: desc = '' # type of representation to be used: hview buf = self.to_bytes() if self.REPR_MAXLEN > 0 and len(buf) > self.REPR_MAXLEN: val_repr = hview(buf[:self.REPR_MAXLEN]) if val_repr: return ['<%s%s%s :' % (self._name, desc, trans)] + val_repr + [' ...>'] else: return ['<%s%s%s : ...>' % (self._name, desc, trans)] else: val_repr = hview(buf) if val_repr: val_repr[-1] += '>' return ['<%s%s%s :' % (self._name, desc, trans)] + val_repr else: return ['<%s%s%s : >' % (self._name, desc, trans)] def show(self): if self._rep == REPR_HD: return '\n'.join([self.get_hier_abs() * ' ' + l for l in self._repr_hd()]) else: return self.get_hier_abs() * ' ' + self.repr() #--------------------------------------------------------------------------# # Python built-ins override #--------------------------------------------------------------------------# __call__ = get_val __repr__ = repr #if python_implementation != 'PyPy': # PyPy iterator implementation leads to an infinite loop # __iter__() calls __len__(), but here, get_bl() calls __iter__() # __len__ = get_bl #------------------------------------------------------------------------------# # Envelope parent class #------------------------------------------------------------------------------# class Envelope(Element): """ Class for envelopes: special element which acts as a container for other elements (atom, envelope, array, sequence, alt) class attribute: - GEN: tuple of elements which is used to build the envelope content at initialization universal attributes: - content: list of elements, cloned from the GEN tuple - trans: bool, transparency of the envelope - hier: hierarchical level when placed in an envelope automation attribute: - transauto: callable, to automate the determination of envelope's transparency contextual attributes: - env: envelope, container of the current envelope Envelope provides methods identical to Python list and dict in order to manage elements within its content easily """ # hardcoded class name CLASS = 'Envelope' # default transparency DEFAULT_TRANS = False # default attributes value _env = None _hier = 0 _desc = '' _blauto = None _trans = None _transauto = None _GEN = tuple() __attrs__ = ('_env', '_name', '_desc', '_hier', '_blauto', '_trans', '_transauto', '_GEN', '_content', '_by_name', '_by_id', '_it' '_it_saved') def __init__(self, *args, **kw): """Initializes an instance of Envelope Args: *args: nothing or envelope name (str) **kw: name (str): envelope name if no args desc (str): additional envelope description hier (int): envelope hierarchy level trans (bool): envelope transparency GEN (tuple of elements): to override the GEN class attribute content (dict): to broadcast settings into the elements within the content val (None, dict, tuple or list): to broadcast values into the elements within the content, using self.set_val() bl (tuple, list or dict): to broadcast bl into the elements within the content, using self.set_bl() """ # iterator index initialization, required by __iter__() # current iterator index: self._it = 0 # saved iterator indexes, when nested iterations happen self._it_saved = [] # envelope name in kw, or first args if len(args): self._name = str(args[0]) elif 'name' in kw: self._name = str(kw['name']) # if not provided, it's the class name elif not hasattr(self, '_name'): self._name = self.__class__.__name__ # envelope description customization if 'desc' in kw: self._desc = str(kw['desc']) # envelope hierarchy if 'hier' in kw: self._hier = kw['hier'] # envelope transparency if 'trans' in kw: self._trans = kw['trans'] if 'GEN' in kw: GEN, clo = kw['GEN'], False else: GEN, clo = self._GEN, True if self._SAFE_STAT: self._chk_hier() self._chk_trans() self._chk_gen(GEN) # content list generation self._content, self._by_id, self._by_name = [], [], [] if clo: self.extend([elt.clone() for elt in GEN]) else: self.extend(GEN) # if a content dict is passed as argument # broadcast it to the given content items if 'content' in kw: #self._log('Envelope.__init__(content):', kw['content']) for i in kw['content']: self.__getitem__(i).set_attr(**kw['content'][i]) # if a val dict is passed as argument # broadcast it to given content items if 'val' in kw: self.set_val( kw['val'] ) # if a bl dict is passed as argument # broadcast it to given content items if 'bl' in kw: self.set_bl( kw['bl'] ) def _chk_gen(self, gen): if not isinstance(gen, tuple) or \ not all([isinstance(elt, Element) for elt in gen]): raise(EltErr('{0} [_chk_gen]: invalid envelope generator GEN'\ .format(self._name))) #--------------------------------------------------------------------------# # envelope, hierarchy and selection routines #--------------------------------------------------------------------------# # no change from Element #--------------------------------------------------------------------------# # format routines #--------------------------------------------------------------------------# def set_val(self, vals): """Set the raw values to the given elements of the content of self Args: vals (None, tuple, list or dict) : tuple or list of all element's value or dict of element's name, element's value or dict of element's index, element's value Returns: None Raises: EltErr : if element's name, index or value are invalid, or value setting raises """ if vals is None: [elt.set_val(None) for elt in self.__iter__()] elif isinstance(vals, (tuple, list)): for ind, elt in enumerate(self.__iter__()): elt.set_val(vals[ind]) elif isinstance(vals, dict): # ordered values is sometimes required, depending of the structure # -> happens in particular when an Alt() is present in the envelope vals_ind = {self._by_name.index(k): v for (k, v) in vals.items() \ if isinstance(k, str_types)} if vals_ind: if len(vals_ind) == len(vals): vals = vals_ind else: vals = {k: v for (k, v) in vals.items() \ if isinstance(k, integer_types)} vals.update(vals_ind) for k in sorted(vals.keys()): self.__setitem__(k, vals[k]) elif self._SAFE_STAT: raise(EltErr('{0} [set_val]: vals type is {1}, expecting None, '\ 'tuple, list or dict'.format(self._name, type(vals).__name__))) def get_val(self): """Returns the list of values of all the elements of the content of self Args: None Returns: value (list) : list of values computed Raises: EltErr : if one element within the content raises """ return [elt() for elt in self.__iter__()] def get_val_d(self): """Returns the dict of element names and values of the content of self Wanrning: in case several elements have the same name, the returned value won't be complete. Args: None Returns: value (dict) : dict of names and values Raises: EltErr : if one element within the content raises """ return {elt._name: elt.get_val_d() for elt in self.__iter__()} def set_bl(self, bl): """Set the raw bit length to the given elements of the content of self Args: bl (tuple, list or dict) : tuple or list of all element's bitlen or dict of element's name, element's bitlen or dict of element's index, element's bitlen Returns: None Raises: EltErr : if element's name, index or bl are invalid, or bit length setting raises """ if isinstance(bl, (tuple, list)): for ind, elt in enumerate(self.__iter__()): elt.set_bl(bl[ind]) elif isinstance(bl, dict): for key, val in bl.items(): self.__getitem__(key).set_bl(val) elif self._SAFE_STAT: raise(EltErr('{0} [set_bl]: bl type is {1}, expecting tuple, list '\ 'or dict'.format(self._name, type(bl).__name__))) def set_blauto(self, blauto=None): """Set an automation for the length in bits of self, used only when mapping an external buffer to it. Args: blauto (callable) : automate the bl computation, call blauto() to compute the bit length, default to None Returns: None Raises: EltErr : if self._SAFE_STAT is enabled and blauto is not a callable """ if blauto is None: try: del self._blauto except Exception: pass else: if self._SAFE_STAT and not callable(blauto): raise(EltErr('{0} [set_blauto]: blauto type is {1}, expecting callable'\ .format(self._name, type(blauto).__name__))) self._blauto = blauto def get_bl(self): """Returns the total length in bits of self Args: None Returns: bl (int) : length in bits computed (sum of the content) Raises: EltErr : if one element within the content raises """ if self.get_trans(): return 0 else: return sum([elt.get_bl() for elt in self.__iter__()]) def reautomate(self): """Reset all attributes of the element which have an automation within the content of self Args: None Returns: None """ if self._transauto is not None and self._trans is not None: del self._trans [elt.reautomate() for elt in self._content] #--------------------------------------------------------------------------# # conversion routines #--------------------------------------------------------------------------# def _to_pack(self): """Produces a list of tuples (type, val, bl) ready to be packed with pack_val() """ if not self.get_trans(): pl = [] [pl.extend(elt._to_pack()) for elt in self.__iter__()] return pl else: return [] def _from_char(self, char): """Dispatch the consumption of a Charpy intance to the elements within the content """ # TODO: in cases some ranges of elt within self have a fixed _bl # it would be more efficient to unpack them in a single shot, # especially if these are int / uint on 8, 16, 32, 64 bits if self.get_trans(): return # truncate char if length automation is set if self._blauto is not None: char_lb = char._len_bit char._len_bit = char._cur + self._blauto() if char._len_bit > char_lb: raise(EltErr('{0} [_from_char]: bit length overflow'.format(self._name))) # for elt in self.__iter__(): elt._from_char(char) # # in case of length automation, set the original length back if self._blauto is not None: char._len_bit = char_lb #--------------------------------------------------------------------------# # copy / cloning routines #--------------------------------------------------------------------------# def get_attrs(self): """Returns the dictionnary of universal attributes of self and the elements within its content Args: None Returns: attrs (dict) : dictionnary of attributes """ return {'name' : self._name, 'desc' : self._desc, 'hier' : self._hier, 'trans' : self._trans, 'content': {elt._name: elt.get_attrs() for elt in self._content}} def get_attrs_all(self): """Returns the dictionnary of all attributes of self and the elements within its content Args: None Returns: attrs (dict) : dictionnary of attributes """ return {'env' : self._env, 'name' : self._name, 'desc' : self._desc, 'hier' : self._hier, 'trans' : self._trans, 'transauto': self._transauto, 'content' : {elt._name: elt.get_attrs_all() for elt in self._content}} def set_attrs(self, **kw): """Updates the attributes of self and the elements within its content Args: kw (dict): dict of attributes and associated values attributes can be name, desc, hier, trans, bl, val and content Returns: None """ if 'name' in kw and isinstance(kw['name'], str): self._name = kw['name'] if 'desc' in kw and isinstance(kw['desc'], str) and kw['desc'] != self.__class__._desc: self._desc = str(kw['desc']) if 'hier' in kw and kw['hier'] != self.__class__._hier: self._hier = kw['hier'] if 'trans' in kw and kw['trans'] != self.__class__._trans: self._trans = kw['trans'] # if self._SAFE_STAT: self._chk_hier() self._chk_trans() # if 'content' in kw: for name, attrs in kw['content'].items(): self.__getitem__(name).set_attrs(**attrs) if 'bl' in kw: self.set_bl(kw['bl']) if 'val' in kw: self.set_val(kw['val']) def clone(self): """Produces an independent clone of self Args: None Returns: clone (self.__class__ instance) """ kw = {} if self._desc != self.__class__._desc: kw['desc'] = self._desc if self._hier != self.__class__._hier: kw['hier'] = self._hier if self._trans != self.__class__._trans: kw['trans'] = self._trans # substitute the Envelope generator with clones of the current # envelope's content kw['GEN'] = tuple([elt.clone() for elt in self._content]) return self.__class__(self._name, **kw) #--------------------------------------------------------------------------# # Python list / dict methods emulation #--------------------------------------------------------------------------# def append(self, elt): """Append the element `elt' at the end of the content of self Args: elt (element) : element to be appended Returns: None Raises: EltErr : if self._SAFE_STAT is enabled and the type of `elt' is not Element """ if self._SAFE_STAT and not isinstance(elt, Element): raise(EltErr('{0} [append]: arg type is {1}, expecting element'\ .format(self._name, type(elt).__name__))) # append elt to content self._content.append(elt) # populate by_id and by_name list self._by_id.append(id(elt)) self._by_name.append(elt._name) # inform elt of its new envelope elt.set_env(self) def count(self, name): """Count the number of elements with name `name' in the content of self Args: name (str) : name to be used for counting Returns: cnt (int): number of iteration of `name' Raises: EltErr : if name `name' has not the right type """ try: return self._by_name.count(name) except Exception as err: raise(EltErr('{0} [count]: {1}'.format(self._name, err))) def extend(self, elt_iter): """Append the list of elements `elt_iter' at the end of the content of self Args: elt_iter (iterable of elements) : list of elements to be appended Returns: None Raises: EltErr : if self._SAFE_STAT is enabled and the types produced by `elt_iter' is not Element """ for elt in elt_iter: if self._SAFE_STAT and not isinstance(elt, Element): raise(EltErr('{0} [extend]: iterated arg type is {1}, expecting element'\ .format(self._name, type(elt).__name__))) self._content.append(elt) self._by_id.append(id(elt)) self._by_name.append(elt._name) elt.set_env(self) def index(self, elt): """Provide the index of the element `elt' within the content of self Args: elt (element) : element to be looked-up in the envelope Returns: ind (int) : index of the element within the envelope Raises: EltErr : element `elt' is not in the content """.format(self.__class__.__name__) try: return self._by_id.index(id(elt)) except Exception as err: raise(EltErr('{0} [index]: {1}'.format(self._name, err))) def insert(self, index, elt): """Insert the element `elt' at the given index in the content of self Args: index (int) : index where to insert `elt' elt (element) : element to be inserted Returns: None Raises: EltErr : if self._SAFE_STAT is enabled and the type of `elt' is not Element, or if insertion at the given index fails """ if self._SAFE_STAT and not isinstance(elt, Element): raise(EltErr('{0} [insert]: arg type is {1}, expecting element'\ .format(self._name, type(elt).__name__))) try: self._content.insert(index, elt) except Exception as err: raise(EltErr('{0} [insert]: {1}'.format(self._name, err))) else: self._by_name.insert(index, elt._name) self._by_id.insert(index, id(elt)) elt.set_env(self) def pop(self): """Pop the last element of the content of self Args: None Returns: elt (element) : last element of the content """ try: elt = self._content.pop() except Exception as err: raise(EltErr('{0} [pop]: {1}'.format(self._name, err))) else: # remove it from by_id and by_name lists self._by_id.pop() self._by_name.pop() # elt has no envelope anymore elt.set_env(None) # return it return elt def remove(self, elt): """Remove the element `elt' from the content of self Args: elt (element) : element to be removed Returns: None Raises: EltErr : if element `elt' is not in the content """ try: ind = self._by_id.index(id(elt)) except Exception as err: raise(EltErr('{0} [remove]: {1}'.format(self._name, err))) else: del self._content[ind], self._by_id[ind], self._by_name[ind] elt.set_env(None) def replace(self, old, new): """Replace the element `old' with the element `new' in the content of self Args: old (element) : element to be removed new (element) : element to be inserted Returns: None Raises: EltErr : if self._SAFE_STAT is enabled and the type of `new' is not Element, or if element `old' is not in the content """ if self._SAFE_STAT and not isinstance(new, Element): raise(EltErr('{0} [replace]: new type is {1}, expecting element'\ .format(self._name, type(new).__name__))) # try: ind = self._by_id.index(id(old)) except Exception as err: raise(EltErr('{0} [replace] error with old: {1}'.format(self._name, err))) else: # remove old del self._content[ind], self._by_name[ind], self._by_id[ind] old.set_env(None) # insert new self._content.insert(ind, new) self._by_name.insert(ind, new._name) self._by_id.insert(ind, id(new)) new.set_env(self) new.set_hier(old.get_hier()) def clear(self): """Clear the content of self Args: None Returns: None """ if python_version < 3: del self._content[:] del self._by_id[:] del self._by_name[:] else: self._content.clear() self._by_id.clear() self._by_name.clear() def __iter__(self): self._it_saved.append(self._it) self._it = 0 return self def __next__(self): if self._it >= len(self._content): if self._it_saved: # in case of nested iteration self._it = self._it_saved.pop() + 1 raise(StopIteration()) else: it = self._it self._it += 1 if self.ENV_SEL_TRANS: # do not take element transparency into account return self._content[it] elif not self._content[it].get_trans(): # non-transparent element return self._content[it] else: # transparent element, pass it and try the next one return self.__next__() if python_version < 3: next = __next__ def __getitem__(self, key): if isinstance(key, str_types): try: return self._content[self._by_name.index(key)] except Exception as err: raise(EltErr('{0} [__getitem__] str item: {1}'.format(self._name, err))) elif isinstance(key, integer_types): #print(self._name, self._content, len(self._content), key) #len(self), key) try: return self._content[key] except Exception as err: raise(EltErr('{0} [__getitem__] int item: {1}'.format(self._name, err))) elif isinstance(key, slice): # a new `slice' envelope is produced # _env and _hier attributes of elements in the content of self are not # updated as the slice envelope is not supposed to become the new home # of those slice_env = Envelope('slice') try: slice_env._content = self._content[key] except Exception as err: raise(EltErr('{0} [__getitem__] slice item: {1}'.format(self._name, err))) else: slice_env._by_name = self._by_name[key] slice_env._by_id = self._by_id[key] return slice_env else: raise(EltErr('{0} [__getitem__]: envelope item must be int, str or slice'\ .format(self._name))) def __setitem__(self, key, val): if isinstance(key, str_types): try: self._content[self._by_name.index(key)].set_val(val) except Exception as err: raise(EltErr('{0} [__setitem__] error with key {1!r}, val {2!r}: {3}'\ .format(self._name, key, val, err))) elif isinstance(key, integer_types): try: self._content[key].set_val(val) except Exception as err: raise(EltErr('{0} [__setitem__] error with key {1!r}, val {2!r}: {3}'\ .format(self._name, key, val, err))) elif isinstance(key, slice): try: [elt.set_val(val[i]) for (i, elt) in enumerate(self._content[key])] except Exception as err: raise(EltErr('{0} [__setitem__] error with key {1!r}, val {2!r}: {3}'\ .format(self._name, key, val, err))) else: raise(EltErr('{0} [__setitem__]: envelope item must be int, str or slice'\ .format(self._name))) def __delitem__(self, key): if isinstance(key, str_types): try: ind = self._by_name.index(key) except Exception as err: raise(EltErr('{0} [__delitem__] str item: {1}'.format(self._name, err))) else: self._content[ind]._env = None del self._content[ind], self._by_name[ind], self._by_id[ind] elif isinstance(key, integer_types): try: self._content[key]._env = None except Exception as err: raise(EltErr('{0} [__delitem__] int item: {1}'.format(self._name, err))) else: del self._content[key], self._by_name[key], self._by_id[key] elif isinstance(key, slice): try: [elt.set_env(None) for elt in self._content[key]] except Exception as err: raise(EltErr('{0} [__delitem__] slice item: {1}'.format(self._name, err))) else: del self._content[key], self._by_name[key], self._by_id[key] else: raise(EltErr('{0} [__delitem__]: envelope item must be int, str or slice'\ .format(self._name))) #--------------------------------------------------------------------------# # representation routines #--------------------------------------------------------------------------# def repr(self): # element transparency if self.get_trans(): trans = ' [transparent]' else: trans = '' # additional description if self._desc: desc = ' [%s]' % self._desc else: desc = '' # return '<%s%s%s : %s>' % \ (self._name, desc, trans, ''.join(map(repr, self._content))) def show(self): # element transparency if self.get_trans(): trans = ' [transparent]' else: trans = '' # additional description if self._desc: desc = ' [%s]' % self._desc else: desc = '' # return '\n '.join( [self.get_hier_abs()*' ' + '### %s%s%s ###' % (self._name, desc, trans)] + \ [elt.show().replace('\n', '\n ') for elt in self.__iter__()]) #--------------------------------------------------------------------------# # Python built-ins override #--------------------------------------------------------------------------# __call__ = get_val __repr__ = repr #if python_implementation != 'PyPy': # PyPy iterator implementation lead to an infinite loop # __iter__() calls __len__(), but here, get_bl() calls __iter__() # __len__ = get_bl #--------------------------------------------------------------------------# # json interface #--------------------------------------------------------------------------# if _with_json: def _from_jval(self, val): if not isinstance(val, list): raise(EltErr('{0} [_from_jval]: invalid format, {1!r}'.format(self._name, val))) i = 0 for e in self._content: if not e.get_trans(): try: e._from_jval_wrap(val[i]) except Exception: break else: i += 1 # ensure all non-transparent elements were set for e in self._content[1+self._content.index(e):]: if not e.get_trans() and e.get_bl(): raise(EltErr('{0} [_from_jval]: missing elements, {1} ...'\ .format(self._name, e._name))) def _to_jval(self): return [e._to_jval_wrap() for e in self._content if not e.get_trans()] class Array(Element): """ Class for arrays: special element which acts as a container for a list of values for a given immutable element (atom, envelope, array, sequence, alt) class attribute: - GEN: element which is used to build the array template at initialization universal attributes: - tmpl: element, cloned from the GEN, used as proxy for generating an array of elements from values in content - tmpl_val: default value for the template to be used in the content, when not explicitely set - num: number of iterations of the template within the content - content: list of values, formatted for the content - trans: bool, transparency of the array - hier: hierarchical level when placed within an envelope automation attribute: - numauto: callable, to automate the determination of the number of template's iteration - blauto: callable, to automate the length in bits to be decoded - transauto: callable, to automate the determination of array's transparency contextual attributes: - env: envelope, container of the array Array provides methods identical to Python list and dict in order to manage Element's instances within its content easily """ # hardcoded class name CLASS = 'Array' # default transparency DEFAULT_TRANS = False # default attributes value _env = None _hier = 0 _desc = '' _trans = None _transauto = None _num = None _numauto = None _blauto = None _GEN = Atom() __attrs__ = ('_env', '_name', '_desc', '_hier', '_trans', '_transauto', '_num', '_numauto', '_blauto', '_GEN', '_tmpl', '_tmpl_val', '_tmpl_bl', '_tmpl_pack', '_val', '_it', '_it_saved') def __init__(self, *args, **kw): """Initializes an instance of Array Args: *args: nothing or instance name (str) **kw: name (str): array name if no args desc (str): additional array description hier (int): array hierarchy level trans (bool): array transparency GEN (element): to override the GEN class attribute tmpl_val (element's value): to set a default value for the template generated from GEN tmpl_bl (element's bl): to set default bit length for the template generated from GEN num (int): number of iteration within the array content val (None, tuple, list or dict): values to be set in the array """ # iterator index initialization, required by __iter__() # current iterator index: self._it = 0 # saved iterator indexes, when nested iterations happen self._it_saved = [] # array name in kw, or first args if len(args): self._name = str(args[0]) elif 'name' in kw: self._name = str(kw['name']) # if not provided, it's the class name elif not hasattr(self, '_name'): self._name = self.__class__.__name__ # array description customization if 'desc' in kw: self._desc = str(kw['desc']) # array hierarchy if 'hier' in kw: self._hier = kw['hier'] # array transparency if 'trans' in kw: self._trans = kw['trans'] if 'GEN' in kw: GEN, clo = kw['GEN'], False else: GEN, clo = self._GEN, True if self._SAFE_STAT: self._chk_hier() self._chk_trans() if not isinstance(GEN, Element): raise(EltErr('{0} [__init__]: invalid array generator GEN'\ .format(self._name))) if clo: self._tmpl = GEN.clone() else: self._tmpl = GEN self._tmpl._env = self # setting default values and format for the template element if 'tmpl_val' in kw: try: self._tmpl.set_val(kw['tmpl_val']) except Exception as err: raise(EltErr('{0} [__init__] set template value error: {1}'\ .format(self._name, err))) if 'tmpl_bl' in kw: try: self._tmpl.set_bl(kw['tmpl_bl']) except Exception as err: raise(EltErr('{0} [__init__] set template bl error: {1}'\ .format(self._name, err))) # set default value, and values container self._tmpl_val = self._tmpl() self._tmpl_bl = self._tmpl.get_bl() self._tmpl_pack = self._tmpl._to_pack() self._val = [] # array number of content if 'num' in kw: self.set_num(kw['num']) # values in the array if 'val' in kw: self.set_val(kw['val']) #--------------------------------------------------------------------------# # envelope, hierarchy and selection routines #--------------------------------------------------------------------------# # nothing changes from Element #--------------------------------------------------------------------------# # format routines #--------------------------------------------------------------------------# def set_val(self, vals): """Set an array of raw values according to the template Args: vals (None, tuple, list or dict) : tuple / list of values or dict of array's index, array's value Returns: None Raises: EltErr : if array's index or value is invalid """ if vals is None: self._val = [] # elif isinstance(vals, dict): max_ind = max(vals.keys()) if self._SAFE_STAT: # ensure the val dict indexes are valid if not all([isinstance(k, integer_types) for k in vals]): raise(EltErr('{0} [set_val] vals keys are not all integers'\ .format(self._name))) # ensure the max index does not overflow a fixed max size if self._num is not None and max_ind >= self._num: raise(EltErr('{0} [set_val] vals index {1} overflow (max {2})'\ .format(self._name, max_ind, self._num))) # in case the current value self._val does not goes up to the max index # just extend it with the default value if len(self._val) <= max_ind: self._val.extend( (1+max_ind-len(self._val)) * (self._tmpl_val, ) ) for i, v in vals.items(): self._tmpl.set_val(v) self._val[i] = self._tmpl.get_val() # reset the template's value self._tmpl.set_val(None) # elif isinstance(vals, (tuple, list)): if self._SAFE_STAT and self._num is not None and len(vals) != self._num: # ensure vals length does not overflow a fixed number of iteration raise(EltErr('{0} [set_val] invalid number of values: {1} instead of {2}'\ .format(self._name, len(vals), self._num))) # self._val = [] for v in vals: self._tmpl.set_val(v) self._val.append(self._tmpl()) # reset the template's value self._tmpl.set_val(None) # else: raise(EltErr('{0} [set_val]: vals type is {1}, expecting None, tuple, list or dict'\ .format(self._name, type(vals).__name__))) def get_val(self): """Returns the list of values of self according to its template Args: None Returns: value (list) : array of values """ return self._val # for array element, no dict to be returned, but just the standard list of values get_val_d = get_val def set_num(self, num=None): """Set the raw number of iteration of the template in the array's value Args: num (int) : raw number of iteration, default to None Returns: None Raises: EltErr : if self._SAFE_STAT is enabled and bitlen is not integer """ if num is None: try: del self._num except Exception: pass else: if self._SAFE_STAT and not isinstance(num, integer_types): raise(EltErr('{0} [set_num]: num type is {1}, expecting integer'\ .format(self._name, type(num).__name__))) self._num = num # clean up extra values if already set if len(self._val) > num: del self._val[num:] # extend values if not already set elif len(self._val) < num: self._val.extend( (num-len(self._val)) * (self._tmpl_val, ) ) def set_numauto(self, numauto=None): """Set an automation for the number of iteration of the template in the array's value, used only when mapping an external buffer to self Args: numauto (callable) : automate the num computation, call numauto() to compute num, default to None Returns: None Raises: EltErr : if self._SAFE_STAT is enabled and numauto is not a callable """ if numauto is None: try: del self._numauto except Exception: pass else: if self._SAFE_STAT and not callable(numauto): raise(EltErr('{0} [set_numauto]: numauto type is {1}, expecting callable'\ .format(self._name, type(numauto).__name__))) self._numauto = numauto def get_num(self): """Returns the number of iterations of the template in the array's value Args: None Returns: num (int) : number of elements, default to the number of values """ # follow the value resolution order: # 1) raw num if self._num is not None: return self._num # 2) num automation: only when parsing buffers (in _from_char) # see _from_char() # 3) no num defined, return the num from the values already set else: return len(self._val) def set_blauto(self, blauto=None): """Set an automation for the length in bits of self, used only when mapping an external buffer to it. Args: blauto (callable) : automate the bl computation, call blauto() to compute the bit length, default to None Returns: None Raises: EltErr : if self._SAFE_STAT is enabled and blauto is not a callable """ if blauto is None: try: del self._blauto except Exception: pass else: if self._SAFE_STAT and not callable(blauto): raise(EltErr('{0} [set_blauto]: blauto type is {1}, expecting callable'\ .format(self._name, type(blauto).__name__))) self._blauto = blauto def get_bl(self): """Returns the length in bits of self Args: None Returns: bl (int) : length in bits computed Raises: EltErr : if one element within the content raises """ if self.get_trans(): return 0 else: ret = [] for v in self._val: if v == self._tmpl_val: ret.append(self._tmpl_bl) else: self._tmpl.set_val(v) ret.append(self._tmpl.get_bl()) self._tmpl.set_val(None) return sum(ret) def reautomate(self): """Reset all attributes of self and its template which have an automation Args: None Returns: None """ if self._transauto is not None and self._trans is not None: del self._trans if self._numauto is not None and self._num is not None: del self._num self._tmpl.reautomate() #--------------------------------------------------------------------------# # conversion routines #--------------------------------------------------------------------------# def _to_pack(self): """Produces a list of tuple ready to be packed with pack_val() from the array's values through the template """ if not self.get_trans(): if self._SAFE_STAT and self._num is not None and len(self._val) != self._num: raise(EltErr('{0} [_to_pack] invalid number of values: {1} instead of {2}'\ .format(self._name, len(self._val), self._num))) pl = [] for v in self._val: if v == self._tmpl_val: pl.extend(self._tmpl_pack) else: self._tmpl.set_val(v) pl.extend(self._tmpl._to_pack()) self._tmpl.set_val(None) return pl else: return [] def _from_char(self, char): """Dispatch the consumption of a Charpy intance to the values within the array through the template """ if self.get_trans(): return # 1) determine the number of iteration of the template within the array if self._numauto is not None: num = self._numauto() if self._SAFE_DYN and not isinstance(num, integer_types): raise(EltErr('{0} [_from_char]: num type produced is {1}, expecting integer'\ .format(self._name, type(num).__name__))) elif self._num is not None: num = self._num else: # num is None, _from_char will consume the charpy instance until # it raises num = None # 2) truncate char if length automation is set if self._blauto is not None: char_lb = char._len_bit char._len_bit = char._cur + self._blauto() if char._len_bit > char_lb: raise(EltErr('{0} [_from_char]: bit length overflow'.format(self._name))) # 3) init value self._val = [] # 4) consume char and fill in self._val if num is not None: for i in range(num): self._tmpl._from_char(char) self._val.append(self._tmpl()) else: # there is no predefined limit in the number of iteration # consume the charpy instance until its empty and raises while True: # remember charpy cursor position, to restore it when it raises cur = char._cur try: self._tmpl._from_char(char) except CharpyErr: char._cur = cur break else: self._val.append(self._tmpl()) self._tmpl.set_val(None) # 5) in case of length automation, set the original length back if self._blauto is not None: char._len_bit = char_lb #--------------------------------------------------------------------------# # copy / cloning routines #--------------------------------------------------------------------------# def get_attrs(self): """Returns the dictionnary of universal attributes of self and its template Args: None Returns: attr (dict) : dictionnary of attributes """ return {'name' : self._name, 'desc' : self._desc, 'hier' : self._hier, 'trans' : self._trans, 'num' : self._num, 'val' : self._val, 'tmpl' : self._tmpl.get_attrs(), 'tmpl_val': self._tmpl_val, 'tmpl_bl' : self._tmpl_bl} def get_attrs_all(self): """Returns the dictionnary of all attributes of self and its template Args: None Returns: attr (dict) : dictionnary of attributes """ return {'name' : self._name, 'desc' : self._desc, 'hier' : self._hier, 'trans' : self._trans, 'transauto': self._transauto, 'num' : self._num, 'numauto' : self._numauto, 'val' : self._val, 'tmpl' : self._tmpl.get_attrs_all(), 'tmpl_val' : self._tmpl_val, 'tmpl_bl' : self._tmpl_bl} def set_attrs(self, **kw): """Updates the attributes of self and its template Args: kw (dict): dict of attributes and associated values attributes can be name, desc, hier, trans, tmpl, val and num Returns: None """ if 'name' in kw and isinstance(kw['name'], str): self._name = kw['name'] if 'desc' in kw and isinstance(kw['desc'], str) and kw['desc'] != self.__class__._desc: self._desc = kw['desc'] if 'hier' in kw and kw['hier'] != self.__class__._hier: self._hier = kw['hier'] if 'trans' in kw and kw['trans'] != self.__class__._trans: self._trans = kw['trans'] # if self._SAFE_STAT: self._chk_hier() self._chk_trans() # if 'tmpl' in kw: self._tmpl.set_attrs(**kw['tmpl']) self._tmpl_val = self._tmpl() self._tmpl_bl = self._tmpl.get_bl() self._tmpl_pack = self._tmpl._to_pack() # if 'val' in kw: self.set_val(kw['val']) if 'num' in kw: self.set_num(kw['num']) def clone(self): """Produces an independent clone of self Args: None Returns: clone (self.__class__ instance) """ kw = {} if self._desc != self.__class__._desc: kw['desc'] = self._desc if self._hier != self.__class__._hier: kw['hier'] = self._hier if self._trans != self.__class__._trans: kw['trans'] = self._trans if self._num != self.__class__._num: kw['num'] = self._num if self._val: kw['val'] = self._val[:] # substitute the Array generator with the current array's template kw['GEN'] = self._tmpl.clone() return self.__class__(self._name, **kw) #--------------------------------------------------------------------------# # Python list / dict methods emulation #--------------------------------------------------------------------------# def append(self, val): """Append the value `val' at the end of the array's value Args: val (depends of self._tmpl) : value to be appended Returns: None Raises: EltErr """ if self._SAFE_STAT: if self._num is not None and len(self._val) == self._num: raise(EltErr('{0} [append] val length {1} overflow (num {2})'\ .format(self._name, 1+len(self._val), self._num))) # use the template to format the value if val != self._tmpl_val: self._tmpl.set_val(val) self._val.append(self._tmpl()) self._tmpl.set_val(None) else: self._val.append(val) # here, .count() is the number of iteration in the array count = get_num def extend(self, vals): """Append the list of values `vals' at the end of the array's value Args: vals (list) : list of values to be appended Returns: None Raises: EltErr """ if self._SAFE_STAT: if self._num is not None and len(vals) > (self._num-len(self._val)): raise(EltErr('{0} [extend]: val length {1} overflow (num {2})'\ .format(self._name, len(self._val)+len(vals), self._num))) # use the template to format the values for val in vals: if val != self._tmpl_val: self._tmpl.set_val(val) self._val.append(self._tmpl()) else: self._val.append(val) self._tmpl.set_val(None) def index(self, val): """Provide the index of the first iteration of value `val' within the array's value Args: val (depends of self._tmpl) : value to get the index of Returns: ind (int) : index of the value within the array Raises: EltErr : if value `val' is not in self._val """ try: return self._val.index(val) except Exception as err: raise(EltErr('{0} [index]: {1}'.format(self._name, err))) def insert(self, index, val): """Insert the value `val' at the given index in array's value Args: index (int) : index where to insert `val' val (depends of self._tmpl) : value to be inserted Returns: None Raises: EltErr : if insertion at the given index fails """ if self._SAFE_STAT: if self._num is not None and len(self._val) == self._num: raise(EltErr('{0} [insert] val length {1} overflow (num {2})'\ .format(self._name, 1+len(self._val), self._num))) # use the template to format the value if val != self._tmpl_val: self._tmpl.set_val(val) val = self._tmpl() self._tmpl.set_val(None) try: self._val.insert(index, val) except Exception as err: raise(EltErr('{0} [insert]: {1}'.format(self._name, err))) def pop(self): """Pop the last value of the array wrapped within the its template Args: None Returns: elt (Element) : last element of the instance """ if self._SAFE_STAT and self._num is not None and len(self._val) == self._num: raise(EltErr('{0} [pop] val length {1} underflow (num {2})'\ .format(self._name, len(self._val)-1, self._num))) try: val = self._val.pop() except Exception as err: raise(EltErr('{0} [pop]: {1}'.format(self._name, err))) else: clone = self._tmpl.clone() clone._val = val return clone def remove(self, val): """Remove the first iteration of value `val' in the array's value Args: val (depends of self._tmpl) : value to be removed Returns: None Raises: EltErr : if value `val' is not in {0} """ if self._SAFE_STAT and self._num is not None and len(self._val) == self._num: raise(EltErr('{0} [remove] val length {1} underflow (num {2})'\ .format(self._name, len(self._val)-1, self._num))) try: self._val.remove(val) except Exception as err: raise(EltErr('{0} [remove]: {1}'.format(self._name, err))) def replace(self, old, new): """Replace the value `old' with the value `new' in the array's value Args: old (depends of self._tmpl) : value to be removed new (depends of self._tmpl) : value to be inserted Returns: None Raises: EltErr : if element `old' is not in self._val """ try: ind = self._val.index(old) except Exception as err: raise(EltErr('{0} [replace] invalid old: {1}'.format(self._name, err))) # use the template to format the value if new != self._tmpl_val: self._tmpl.set_val(new) new = self._tmpl() self._tmpl.set_val(None) del self._val[ind] self._val.insert(ind, new) def clear(self): """Clear the values of self Args: None Returns: None """ if python_version < 3: del self._val[:] else: self._val.clear() def __iter__(self): self._it_saved.append(self._it) self._it = 0 return self def __next__(self): if self._it >= len(self._val) or self._tmpl.get_trans(): if self._it_saved: # in case of nested iteration self._it = self._it_saved.pop() + 1 raise(StopIteration()) else: it = self._it self._it += 1 clone = self._tmpl.clone() clone.set_val(self._val[it]) clone._env = self return clone if python_version < 3: next = __next__ def __getitem__(self, key): if isinstance(key, integer_types): try: val = self._val[key] except Exception as err: raise(EltErr('{0} [__getitem__] int item: {1}'\ .format(self._name, err))) clone = self._tmpl.clone() clone.set_val(val) return clone elif isinstance(key, slice): try: val = self._val[key] except Exception as err: raise(EltErr('{0} [__getitem__] slice item: {1}'\ .format(self._name, err))) slice_env = Array('slice', GEN=self._tmpl.clone()) slice_env.set_val(val) return slice_env else: raise(EltErr('{0} [__getitem__]: array item must be int or slice'.format(self._name))) def __setitem__(self, key, val): if isinstance(key, integer_types): if val != self._tmpl_val: self._tmpl.set_val(val) val = self._tmpl() self._tmpl.set_val(None) try: self._val[key] = val except Exception as err: raise(EltErr('{0} [__setitem__] int item: {1}'\ .format(self._name, err))) elif isinstance(key, slice): for i, k in enumerate(key): try: self.__setitem__(k, val[i]) except Exception as err: raise(EltErr('{0} [__setitem__] slice item: {1}'\ .format(self._name, err))) else: raise(EltErr('{0} [__setitem__]: array item must be int or slice'.format(self._name))) def __delitem__(self, key): if self._SAFE_STAT and self._num is not None and len(self._val) == self._num: raise(EltErr('{0} [__delitem__] val length {1} underflow (num {2})'\ .format(self._name, len(self._val)-1, self._num))) if isinstance(key, integer_types): try: del self._val[key] except Exception as err: raise(EltErr('{0} [__delitem__] int item: {1}'.format(self._name, err))) elif isinstance(key, slice): try: del self._val[key] except Exception as err: raise(EltErr('{0} [__delitem__] slice item: {1}'.format(self._name, err))) else: raise(EltErr('{0} [__delitem__]: array item indices must be int or slice'\ .format(self._name))) #--------------------------------------------------------------------------# # representation routines #--------------------------------------------------------------------------# def repr(self): # element transparency if self.get_trans(): trans = ' [transparent]' else: trans = '' # additional description if self._desc: desc = ' [%s]' % self._desc else: desc = '' # return '<%s%s%s : %s>' % \ (self._name, desc, trans, ''.join(map(repr, self.__iter__()))) def show(self): # element transparency if self.get_trans(): trans = ' [transparent]' else: trans = '' # additional description if self._desc: desc = ' [%s]' % self._desc else: desc = '' # return '\n '.join( [self.get_hier_abs()*' ' + '### %s%s%s ###' % (self._name, desc, trans)] + \ [elt.show().replace('\n', '\n ') for elt in self.__iter__()]) #--------------------------------------------------------------------------# # Python built-ins override #--------------------------------------------------------------------------# __call__ = get_val __repr__ = repr #if python_implementation != 'PyPy': # PyPy iterator implementation lead to an infinite loop # __iter__() calls __len__(), but here, get_bl() calls __iter__() # __len__ = get_bl #--------------------------------------------------------------------------# # json interface #--------------------------------------------------------------------------# if _with_json: def _from_jval(self, val): if not isinstance(val, list): raise(EltErr('{0} [_from_jval]: invalid format, {1!r}'.format(self._name, val))) # 1) determine the number of iteration of the template within the array if self._numauto is not None: num = self._numauto() if self._SAFE_DYN and not isinstance(num, integer_types): raise(EltErr('{0} [_from_jval]: num type produced is {1}, expecting integer'\ .format(self._name, type(num).__name__))) elif self._num is not None: num = self._num else: # num is None, from_json will consume the txt until it raises num = None # 2) init value self._val = [] # 3) consume val if num is not None and len(val) != num: raise(EltErr('{0} [_from_jval]: invalid number of values: {1} instead of {2}'\ .format(self._name, len(val), num))) for v in val: self._tmpl._from_jval_wrap(v) self._val.append( self._tmpl.get_val() ) def _to_jval(self): if self._SAFE_STAT and self._num is not None and len(self._val) != self._num: raise(EltErr('{0} [_to_jval] invalid number of values: {1} instead of {2}'\ .format(self._name, len(self._val), self._num))) ret = [] for v in self._val: self._tmpl.set_val(v) ret.append( self._tmpl._to_jval_wrap() ) return ret class Sequence(Element): """ Class for sequences: special element which acts as a container for a list of instances cloned from a mutable template element (atom, envelope, array, sequence, alt) class attribute: - GEN: element which is used to build the sequence template at initialization universal attributes: - tmpl: element, cloned from the GEN, used as proxy for generating a sequence of clones into content, also used as default value - content: list of elements - num: number of iteration of elements within the content - trans: bool, transparency of the sequence - hier: hierarchical level when placed within an envelope automation attribute: - numauto: callable, to automate the determination of the number of content's iteration - blauto: callable, to automate the length in bits to be decoded - transauto: callable, to automate the determination of sequence's transparency contextual attributes: - env: envelope, container of the sequence Sequence provides methods identical to Python list and dict in order to manage Element's instances within its content easily Warning: transparency of certain elements within the Sequence's content is not handled. Elements within the content must not be made transparent (e.g. with set_trans()), but must be remove()d instead. """ # hardcoded class name CLASS = 'Sequence' # default transparency DEFAULT_TRANS = False # default attributes value _env = None _hier = 0 _desc = '' _trans = None _transauto = None _num = None _numauto = None _blauto = None _GEN = Atom() __attrs__ = ('_env', '_name', '_desc', '_hier', '_trans', '_transauto', '_num', '_numauto', '_blauto', '_tmpl', '_content', '_it', '_it_saved') def __init__(self, *args, **kw): """Initializes an instance of Sequence Args: *args: nothing or instance name (str) **kw: name (str): sequence name if no args desc (str): additional sequence description hier (int): sequence hierarchy level trans (bool): sequence transparency GEN (element): to override the GEN class attribute tmpl_val (element's value): to set a default value for the template generated from GEN tmpl_bl (element's bl): to set default bit length for the template generated from GEN num (int): number of iteration within the sequence content val (None, tuple, list or dict): values to be set in the sequence """ # iterator index initialization # current iterator index: self._it = 0 # saved iterator indexes, when nested iterations happen self._it_saved = [] # sequence envelope self._env = None # sequence name in kw, or first args if len(args): self._name = str(args[0]) elif 'name' in kw: self._name = str(kw['name']) # if not provided, it's the class name elif not hasattr(self, '_name'): self._name = self.__class__.__name__ # sequence description customization if 'desc' in kw: self._desc = str(kw['desc']) # sequence hierarchy if 'hier' in kw: self.set_hier(kw['hier']) # sequence transparency if 'trans' in kw: self.set_trans(kw['trans']) if 'GEN' in kw: GEN, clo = kw['GEN'], False else: GEN, clo = self._GEN, True # verifying sequence generator if self._SAFE_STAT: self._chk_hier() self._chk_trans() if not isinstance(GEN, Element): raise(EltErr('{0} [__init__]: invalid sequence generator GEN'\ .format(self._name))) if clo: self._tmpl = GEN.clone() else: self._tmpl = GEN self._tmpl._env = self if 'tmpl_val' in kw: try: self._tmpl.set_val(kw['tmpl_val']) except Exception: raise(EltErr('{0} [__init__] set template value error: {1}'\ .format(self._name, err))) if 'tmpl_bl' in kw: try: self._tmpl.set_bl(kw['tmpl_bl']) except Exception: raise(EltErr('{0} [__init__] set template bl error: {1}'\ .format(self._name, err))) # set default value, and values container self._content = [] # sequence number of content if 'num' in kw: self.set_num(kw['num']) # values in the sequence if 'val' in kw: self.set_val(kw['val']) #--------------------------------------------------------------------------# # envelope, hierarchy and selection routines #--------------------------------------------------------------------------# # nothing changes from Element #--------------------------------------------------------------------------# # format routines #--------------------------------------------------------------------------# def set_val(self, vals): """Set a sequence of template clones with the given values Args: vals (None, tuple, list or dict) : list of values or dict of element's index, element's value Returns: None Raises: EltErr : if element's index or value is invalid """ # if vals is None: self._content = [] # elif isinstance(vals, dict): max_ind = max(vals.keys()) if self._SAFE_STAT: # ensure the val dict indexes are valid if not all([isinstance(k, integer_types) for k in vals]): raise(EltErr('{0} [set_val] vals keys are not all integers'\ .format(self._name))) # ensure the max index does not overflow a fixed max size if self._num is not None and max_ind >= self._num: raise(EltErr('{0} [set_val] vals index {1} overflow (max {2})'\ .format(self._name, max_ind, self._num))) # in case the current content self._content does not goes up to the max # val dict index, just extend it with the template element if len(self._content) <= max_ind: self._content.extend( (1+max_ind-len(self._content)) * (self._tmpl, ) ) # set values at given key indexes for i, v in vals.items(): c = self._content[i] if c == self._tmpl: # in case of tmpl, clone it before assigning the value c = c.clone() c._env = self self._content[i] = c c.set_val(v) # elif isinstance(vals, (tuple, list)): if self._SAFE_STAT and self._num is not None and len(vals) > self._num: # ensure vals length does not overflow a fixed number of iteration raise(EltErr('{0} [set_val] invalid number of values: {1} instead of {2}'\ .format(self._name, len(vals), self._num))) # in case the current content self._content does not goes up to the max # val dict index, just extend it with the template element if len(self._content) < len(vals): self._content.extend( (len(vals)-len(self._content)) * (self._tmpl, ) ) for i, v in enumerate(vals): c = self._content[i] if c == self._tmpl: # in case of tmpl, clone it before assigning the value c = c.clone() c._env = self self._content[i] = c c.set_val(v) # else: raise(EltErr('{0} [set_val]: vals type is {1}, expecting None, tuple, list or dict'\ .format(self._name, type(vals).__name__))) def get_val(self): """Returns the list of values of self Args: None Returns: value (list) : list of values computed """ return [elt() for elt in self._content] def get_val_d(self): """Returns the list of values obtained with get_val_d() from the content of self Wanrning: in case several elements have the same name, the returned value won't be complete. Args: None Returns: value (list) : list of values obtained with get_val_d() Raises: EltErr : if one element within the content raises """ return [elt.get_val_d() for elt in self._content] def set_num(self, num=None): """Set the raw number of iteration of the template in the sequence's content Args: num (int) : raw number of iteration, default to None Returns: None Raises: EltErr : if self._SAFE_STAT is enabled and bitlen is not integer """ if num is None: try: del self._num except Exception: pass else: if self._SAFE_STAT and not isinstance(num, integer_types): raise(EltErr('{0} [set_num]: num type is {1}, expecting integer'\ .format(self._name, type(num).__name__))) self._num = num # clean up extra content if already set if len(self._content) > num: del self._content[num:] # extend content if not already set elif len(self._content) < num: self._content.extend( (num-len(self._content)) * (self._tmpl, ) ) def set_numauto(self, numauto=None): """Set an automation for the number of iteration of the template in the sequence's content, used only when mapping an external buffer to self Args: numauto (callable) : automate the num computation, call numauto() to compute num, default to None Returns: None Raises: EltErr : if self._SAFE_STAT is enabled and numauto is not a callable """ if numauto is None: try: del self._numauto except Exception: pass else: if self._SAFE_STAT and not callable(numauto): raise(EltErr('{0} [set_numauto]: numauto type is {1}, expecting callable'\ .format(self._name, type(numauto).__name__))) self._numauto = numauto def get_num(self): """Returns the number of iterations of the template in the sequence's content Args: None Returns: num (int) : number of elements, default to the number of values """ # follow the value resolution order: # 1) raw num if self._num is not None: return self._num # 2) num automation: only when parsing buffers (in _from_char) # see _from_char() # 3) no num defined, return the num from the content already set else: return len(self._content) def set_blauto(self, blauto=None): """Set an automation for the length in bits of self, used only when mapping an external buffer to it. Args: blauto (callable) : automate the bl computation, call blauto() to compute the bit length, default to None Returns: None Raises: EltErr : if self._SAFE_STAT is enabled and blauto is not a callable """ if blauto is None: try: del self._blauto except Exception: pass else: if self._SAFE_STAT and not callable(blauto): raise(EltErr('{0} [set_blauto]: blauto type is {1}, expecting callable'\ .format(self._name, type(blauto).__name__))) self._blauto = blauto def get_bl(self): """Returns the length in bits of self Args: None Returns: bl (int) : length in bits computed Raises: EltErr : if one element within the content raises """ if self.get_trans(): return 0 else: return sum([elt.get_bl() for elt in self._content]) def reautomate(self): """Reset all attributes of self, its content and its template which have an automation Args: None Returns: None """ if self._transauto is not None and self._trans is not None: del self._trans if self._numauto is not None and self._num is not None: del self._num [elt.reautomate() for elt in self._content if elt != self._tmpl] self._tmpl.reautomate() #--------------------------------------------------------------------------# # conversion routines #--------------------------------------------------------------------------# def _to_pack(self): """Produces a list of tuple ready to be packed with pack_val() from the sequence's content """ if not self.get_trans(): if self._SAFE_STAT and self._num is not None and len(self._content) != self._num: raise(EltErr('{0} [_to_pack]: invalid number of repeated content: {1} instead of {2}'\ .format(self._name, len(self._content), self._num))) pl = [] [pl.extend(elt._to_pack()) for elt in self._content] return pl else: return [] def _from_char(self, char): """Dispatch the consumption of a Charpy intance to the elements within the sequence's content """ if self.get_trans(): return # 1) determine the number of iteration of the template within the sequence if self._numauto is not None: num = self._numauto() if self._SAFE_DYN and not isinstance(num, integer_types): raise(EltErr('{0} [_from_char]: num type produced is {1}, expecting integer'\ .format(self._name, type(num).__name__))) elif self._num is not None: num = self._num else: # num is None, _from_char will consume the charpy instance until # it raises num = None # 2) truncate char if length automation is set if self._blauto is not None: char_lb = char._len_bit char._len_bit = char._cur + self._blauto() if char._len_bit > char_lb: raise(EltErr('{0} [_from_char]: bit length overflow'.format(self._name))) # 3) init content self._content = [] # 4) consume char and fill in self._content if num is not None: for i in range(num): clone = self._tmpl.clone() clone._env = self clone._from_char(char) self._content.append(clone) else: # there is no predefined limit in the number of repeated content # consume the charpy instance until its empty and raises while True: # remember charpy cursor position, to restore it when it raises cur = char._cur clone = self._tmpl.clone() clone._env = self try: clone._from_char(char) except CharpyErr: char._cur = cur break else: self._content.append(clone) # 5) in case of length automation, set the original length back if self._blauto is not None: char._len_bit = char_lb #--------------------------------------------------------------------------# # copy / cloning routines #--------------------------------------------------------------------------# def get_attrs(self): """Returns the dictionnary of universal attributes of self, its template and content Args: None Returns: attr (dict) : dictionnary of attributes """ return {'name' : self._name, 'desc' : self._desc, 'hier' : self._hier, 'trans' : self._trans, 'num' : self._num, 'tmpl' : self._tmpl.get_attrs(), 'content': [elt.get_attrs() for elt in self._content]} def get_attrs_all(self): """Returns the dictionnary of all attributes of self, its content template and its values Args: None Returns: attr (dict) : dictionnary of attributes """ return {'env' : self._env, 'name' : self._name, 'desc' : self._desc, 'hier' : self._hier, 'trans' : self._trans, 'transauto': self._transauto, 'num' : self._num, 'numauto' : self._numauto, 'tmpl' : self._tmpl.get_attrs_all(), 'content' : [elt.get_attrs_all() for elt in self._content]} def set_attrs(self, **kw): """Updates the attributes of self, its template and its content Args: kw (dict): dict of attributes and associated values attributes can be name, desc, hier, trans, tmpl, val and num Returns: None """ if 'name' in kw and isinstance(kw['name'], str): self._name = kw['name'] if 'desc' in kw and isinstance(kw['desc'], str) and kw['desc'] != self.__class__._desc: self._desc = str(kw['desc']) if 'hier' in kw and kw['hier'] != self.__class__._hier: self._hier = kw['hier'] if 'trans' in kw and kw['trans'] != self.__class__._trans: self._trans = kw['trans'] # if self._SAFE_STAT: self._chk_hier() self._chk_trans() # if 'tmpl' in kw: try: self._tmpl.set_attrs(**kw['tmpl']) except Exception as err: raise(EltErr('{0} [set_attrs] invalid tmpl: {1}'.format(self._name, err))) # if 'val' in kw: self.set_val(kw['val']) if 'num' in kw: self.set_num(kw['num']) def clone(self): """Produces an independent clone of self Args: None Returns: clone (self.__class__ instance) """ kw = {} if self._desc != self.__class__._desc: kw['desc'] = self._desc if self._hier != self.__class__._hier: kw['hier'] = self._hier if self._trans != self.__class__._trans: kw['trans'] = self._trans if self._num != self.__class__._num: kw['num'] = self._num # substitute the sequence generator with the current sequence's template kw['GEN'] = self._tmpl.clone() clone = self.__class__(self._name, **kw) # clone all elements within the content clone._content = [elt.clone() for elt in self._content] [elt.set_env(clone) for elt in clone._content] return clone #--------------------------------------------------------------------------# # Python list / dict methods emulation #--------------------------------------------------------------------------# def append(self, elt): """Append the element `elt' at the end of the sequence's content Args: elt (element) : element to be appended Returns: None Raises: EltErr """ if self._SAFE_STAT: if not isinstance(elt, Element): raise(EltErr('{0} [append]: elt type is {1}, expecting element'\ .format(self._name, type(elt).__name__))) if self._num is not None and len(self._content) == self._num: raise(EltErr('{0} [append]: content length {1} overflow (max {2})'\ .format(self._name, 1+len(self._content), self._num))) # self._content.append(elt) elt._env = self # here, .count() is the number of iteration in the sequence count = get_num def extend(self, elts): """Append the list of elements `elts' at the end of sequence's content Args: elts (list) : iterable of elements to be appended Returns: None Raises: EltErr """ if self._SAFE_STAT: if not all([isinstance(elt, Element) for elt in elts]): raise(EltErr('{0} [extend]: elts type must be element'.format(self._name))) elif self._num is not None and len(elts) > (self._num-len(self._content)): raise(EltErr('{0} [extend]: content length {1} overflow (max {2})'\ .format(self._name, len(elts)+len(self._content), self._num))) # self._content.extend(elts) [elt.set_env(self) for elt in elts] def index(self, elt): """Provide the index of the element `elt' within the sequence's content Args: elt (element) : element to get the index of Returns: ind (int) : index of the element within the sequence Raises: EltErr : if `elt' is not in self._content """ try: return self._content.index(elt) except Exception as err: raise(EltErr('{0} [index]: {1}'.format(self._name, err))) def insert(self, index, elt): """Insert the element `elt' at the given index in sequence's content Args: index (int) : index where to insert `elt' elt (element) : element to be inserted Returns: None Raises: EltErr : if `elt' is not an element or if insertion at the given index fails """ if self._SAFE_STAT: if not isinstance(elt, Element): raise(EltErr('{0} [insert]: elt type is {1}, expecting element'\ .format(self._name, type(elt).__name__))) if self._num is not None and len(self._content) == self._num: raise(EltErr('{0} [insert]: content length {1} overflow (max {2})'\ .format(self._name, 1+len(self._content), self._num))) try: self._content.insert(index, elt) except Exception as err: raise(EltErr('{0} [insert]: {1}'.format(self._name, err))) else: elt._env = self def pop(self): """Pop the last element of sequence's content Args: None Returns: elt (Element) : last element of the instance Raises: EltErr : if no element are already set in self """ if self._SAFE_STAT and self._num is not None and len(self._content) == self._num: raise(EltErr('{0} [pop] content length {1} underflow (num {2})'\ .format(self._name, len(self._content)-1, self._num))) try: elt = self._content.pop() except Exception as err: raise(EltErr('{0} [pop]: {1}'.format(self._name, err))) else: if elt == self._tmpl: return elt.clone() else: elt._env = None return elt def remove(self, elt): """Remove the element `elt' from the sequence's content Args: elt (element) : element to be removed Returns: None Raises: EltErr : if `elt' is not in self """ if self._SAFE_STAT and self._num is not None and len(self._content) == self._num: raise(EltErr('{0} [remove] content length {1} underflow (num {2})'\ .format(self._name, len(self._content)-1, self._num))) try: self._content.remove(elt) except Exception as err: raise(EltErr('{0} [remove]: {1}'.format(self._name, err))) else: if elt != self._tmpl: elt._env = None def replace(self, old, new): """Replace the element `old' with the element `new' in the sequence's content Args: old (element) : element to be removed, alternatively, old can be the element's index (int) new (element) : element to be inserted Returns: None Raises: EltErr : if element `old' is not in self or if `new' is not an element """.format(self.__class__.__name__) if isinstance(old, Elt): try: ind = self._content.index(old) except Exception as err: raise(EltErr('{0} [replace] invalid old: {1}'.format(self._name, err))) elif isinstance(old, integer_types): ind = old try: old = self._content[ind] except Exception: raise(EltErr('{0} [replace] invalid old index: {1}'.format(self._name, ind))) elif self._SAFE_STAT: raise(EltErr('{0} [replace]: elt type is {1}, expecting element or index'\ .format(self._name, type(elt).__name__))) del self._content[ind] if old != self._tmpl: old._env = None self._content.insert(ind, new) new._env = self def clear(self): """Clear the content of self Args: None Returns: None """ if python_version < 3: del self._content[:] else: self._content.clear() def __iter__(self): self._it_saved.append(self._it) self._it = 0 return self def __next__(self): if self._it >= len(self._content) or self._tmpl.get_trans(): if self._it_saved: # in case of nested iteration self._it = self._it_saved.pop() + 1 raise(StopIteration()) else: it = self._it self._it += 1 if self.ENV_SEL_TRANS: # do not take element transparency into account return self._content[it] elif not self._content[it].get_trans(): # non-transparent element return self._content[it] else: # transparent element, pass it and try the next one return self.__next__() if python_version < 3: next = __next__ def __getitem__(self, key): if isinstance(key, integer_types): try: return self._content[key] except Exception as err: raise(EltErr('{0} [__getitem__] int item: {1}'.format(self._name, err))) elif isinstance(key, slice): slice_env = Sequence('slice', GEN=self._tmpl.clone()) try: slice_env._content = self._content[key] except Exception as err: raise(EltErr('{0} [__getitem__] slice item: {1}'.format(self._name, err))) return slice_env else: raise(EltErr('{0} [__getitem__]: sequence item must be int or slice'\ .format(self._name))) def __setitem__(self, key, val): if isinstance(key, integer_types): try: elt = self._content[key] except Exception as err: raise(EltErr('{0} [__setitem__] int item: {1}'.format(self._name, err))) if elt == self._tmpl: self._content[key] = self._tmpl.clone() self._content[key].set_val(val) elif isinstance(key, slice): for i, k in enumerate(key): try: self.__setitem__(k, val[i]) except Exception as err: raise(EltErr('{0} [__setitem__] slice item: {1}'\ .format(self._name, err))) else: raise(EltErr('{0} [__setitem__]: sequence item must be int or slice'\ .format(self._name))) def __delitem__(self, key): if self._SAFE_STAT and self._num is not None and len(self._content) == self._num: raise(EltErr('{0} [__delitem__] content length {1} underflow (num {2})'\ .format(self._name, len(self._content), self._num))) if isinstance(key, integer_types): try: elt = self._content[key] except Exception as err: raise(EltErr('{0} [__delitem__] int item: {1}'.format(self._name, err))) del self._content[key] if elt != self._tmpl: elt._env = None elif isinstance(key, slice): try: elts = self._content[key] except Exception as err: raise(EltErr('{0} [__delitem__] slice item: {1}'.format(self._name, err))) del self._content[key] [elt.set_env(None) for elt in elts if elt != self._tmpl] else: raise(EltErr('{0} [__delitem__]: sequence item must be int or slice'\ .format(self._name))) #--------------------------------------------------------------------------# # representation routines #--------------------------------------------------------------------------# def repr(self): # element transparency if self.get_trans(): trans = ' [transparent]' else: trans = '' # additional description if self._desc: desc = ' [%s]' % self._desc else: desc = '' # return '<%s%s%s : %s>' % \ (self._name, desc, trans, ''.join(map(repr, self._content))) def show(self): # element transparency if self.get_trans(): trans = ' [transparent]' else: trans = '' # additional description if self._desc: desc = ' [%s]' % self._desc else: desc = '' # return '\n '.join( [self.get_hier_abs()*' ' + '### %s%s%s ###' % (self._name, desc, trans)] + \ [elt.show().replace('\n', '\n ') for elt in self._content]) #--------------------------------------------------------------------------# # Python built-ins override #--------------------------------------------------------------------------# __call__ = get_val __repr__ = repr #if python_implementation != 'PyPy': # PyPy iterator implementation lead to an infinite loop # __iter__() calls __len__(), but here, get_bl() calls __iter__() # __len__ = get_bl #--------------------------------------------------------------------------# # json interface #--------------------------------------------------------------------------# if _with_json: def _from_jval(self, val): if not isinstance(val, list): raise(EltErr('{0} [_from_jval]: invalid format, {1!r}'.format(self._name, val))) # 1) determine the number of iteration of the template within the array if self._numauto is not None: num = self._numauto() if self._SAFE_DYN and not isinstance(num, integer_types): raise(EltErr('{0} [_from_jval]: num type produced is {1}, expecting integer'\ .format(self._name, type(num).__name__))) elif self._num is not None: num = self._num else: # num is None, from_json will consume the txt until it raises num = None # 2) consume txt and fill in self._content if num is not None and len(val) != num: raise(EltErr('{0} [_from_jval]: invalid number of values: {1} instead of {2}'\ .format(self._name, len(val), num))) # trying to keep potential mutated tmpl from the existing content if len(self._content) < len(val): while len(self._content) < len(val): clone = self._tmpl.clone() self._content.append(clone) clone._env = self elif len(self._content) > len(val): del self._content[len(val):] for i, v in enumerate(val): self._content[i]._from_jval_wrap(v) def _to_jval(self): if self._SAFE_STAT and self._num is not None and len(self._content) != self._num: raise(EltErr('{0} [_to_jval]: invalid number of repeated content: {1} instead of {2}'\ .format(self._name, len(self._content), self._num))) return [e._to_jval_wrap() for e in self._content if not e.get_trans()] class Alt(Element): """ Class for alternatives: special element which acts as a container for a list of alternatives between several elements (atom, envelope, array, sequence, alt) class attribute: - GEN: dict of elements which is used to build all alternatives content at initialization, keys are values that must correspond to the selector element universal attributes: - sel: selector callback to get the value which is used to select one of the alternatives - content: dict of selector values and elements cloned from the GEN dict - trans: bool, transparency of the alternative - hier: hierarchical level when placed in an envelope automation attribute: - transauto: callable, to automate the determination of alternative's transparency contextual attributes: - env: envelope, container of the current alternative Alt provides methods identical to Python list and dict in order to manage elements within its content easily """ # hardcoded class name CLASS = 'Alt' # explicit representation behaviour # if True, returns an explicit representation of the alternative including # its selection value # otherwise, returns directly the representation of the selected alternative REPR_EXPL = True # default transparency DEFAULT_TRANS = False # default element in case of invalid selection value # if set to None, each invalid selection will raise an EltErr DEFAULT = Envelope('none') # default attributes value _env = None _hier = 0 _desc = '' _blauto = None _trans = None _transauto = None _GEN = {} _sel = lambda a, b: None # Warning: # When setting the selection callback as class attribute _sel, prototype is # lambda a, b: ..., both a & b being self, when instantiated # When setting the selection callback during / after initialization, prototype is # lambda a: ..., a being self __attrs__ = ('_env', '_name', '_desc', '_hier', '_blauto', '_trans', '_transauto', '_GEN', '_sel', '_content') def __init__(self, *args, **kw): """Initializes an instance of Alt Args: *args: nothing or alt name (str) **kw: name (str): alt name if no args desc (str): additional alt description hier (int): alt hierarchy level trans (bool): alt transparency GEN (dict of key, elements): to override the GEN class attribute sel (cb): callable to automate the alternative selection warning: this cb must always have a single argument which is self val (None, dict, tuple or list): to broadcast values into the element selected within the content, using self.set_val() bl (tuple, list or dict): to broadcast bl into the element selected within the content, using self.set_bl() """ # alt name in kw, or first args if len(args): self._name = str(args[0]) elif 'name' in kw: self._name = str(kw['name']) # if not provided, it's the class name elif not hasattr(self, '_name'): self._name = self.__class__.__name__ # alt description customization if 'desc' in kw: self._desc = str(kw['desc']) # alt hierarchy if 'hier' in kw: self._hier = kw['hier'] # alt transparency if 'trans' in kw: self._trans = kw['trans'] if 'GEN' in kw: self._GEN = kw['GEN'] # default alternative if 'DEFAULT' in kw: self.DEFAULT = kw['DEFAULT'] elif self.__class__.DEFAULT: self.DEFAULT = self.__class__.DEFAULT.clone() if self._SAFE_STAT: self._chk_hier() self._chk_trans() self._chk_gen(self._GEN) if self.DEFAULT is not None and \ not isinstance(self.DEFAULT, Element): raise(EltErr('{0} [__init__]: invalid DEFAULT element'\ .format(self._name))) # content is populated with clones from GEN in a lazy way # through calling get_alt() self._content = {} # alternative selection callback if 'sel' in kw: self.set_sel( kw['sel'] ) # if a val dict is passed as argument # broadcast it to given content items if 'val' in kw: self.set_val( kw['val'] ) # if a bl dict is passed as argument # broadcast it to given content items if 'bl' in kw: self.set_bl( kw['bl'] ) def _chk_gen(self, gen): if not isinstance(gen, dict) or \ not all([isinstance(elt, Element) for elt in gen.values()]): raise(EltErr('{0} [_chk_gen]: invalid alt generator or content'\ .format(self._name))) #--------------------------------------------------------------------------# # envelope, hierarchy and selection routines #--------------------------------------------------------------------------# # no change from Element #--------------------------------------------------------------------------# # format routines #--------------------------------------------------------------------------# def set_sel(self, cb): """Sets the alternative selection automation for self Args: cb (callable) : automate the alternative selection Returns: None Raises: EltErr : is self._SAFE_STAT enabled and cb is not a callable """ if self._SAFE_STAT and not callable(cb): raise(EltErr('{0} [set_sel]: cb type is {1}, expecting callable'\ .format(self._name, type(cb).__name__))) self._sel = cb def get_sel(self): """Gets the key corresponding to the alternative to be selected """ try: return self._sel(self) except Exception: return None def get_alt(self): """Gets the selected alternative Args: None Returns: elt : selected alternative element Raises: EltErr : if the selection value is invalid and no DEFAULT is set """ sv = self.get_sel() if sv in self._content: elt = self._content[sv] elt.set_env(self.get_env()) return elt elif sv in self._GEN: elt = self._GEN[sv].clone() self.insert(sv, elt) return elt elif self.DEFAULT is not None: self.DEFAULT.set_env(self.get_env()) return self.DEFAULT else: raise(EltErr('{0} [set_val]: invalid selection value {1!r}'\ .format(self._name, sv))) def set_blauto(self, blauto=None): """Set an automation for the length in bits of self, used only when mapping an external buffer to it. Args: blauto (callable) : automate the bl computation, call blauto() to compute the bit length, default to None Returns: None Raises: EltErr : if self._SAFE_STAT is enabled and blauto is not a callable """ if blauto is None: try: del self._blauto except Exception: pass else: if self._SAFE_STAT and not callable(blauto): raise(EltErr('{0} [set_blauto]: blauto type is {1}, expecting callable'\ .format(self._name, type(blauto).__name__))) self._blauto = blauto # standard methods passthrough def set_val(self, val=None): self.get_alt().set_val(val) def _chk_val(self, *args): self.get_alt()._chk_val(*args) #def set_valauto(self, valauto=None): # self.get_alt().set_valauto(valauto) def get_val(self): return self.get_alt().get_val() # for alt element, no dict to be returned, but just the standard list of values def get_val_d(self): alt = self.get_alt() if isinstance(alt, Envelope): return alt.get_val_d() else: return alt.get_val() def set_bl(self, bl=None): self.get_alt().set_bl(bl) def _chk_bl(self, *args): self.get_alt()._chk_bl(*args) #def set_blauto(self, blauto=None): # self.get_alt().set_blauto(blauto) def set_len(self, l=None): self.get_alt().set_len(l) def _get_bl_from_val(self): return self.get_alt()._get_bl_from_val() def get_bl(self): return self.get_alt().get_bl() def set_dic(self, dic=None): self.get_alt().set_dic(dic) def _chk_dic(self, *args): self.get_alt()._chk_dic(*args) #def set_dicauto(self, dicauto=None): # self.get_alt()._chk_dicauto(dicauto) def get_dic(self): return self.get_alt().get_dic() def get_val_dic(self): return self.get_alt().get_val_dic() def reautomate(self): """Resets all attributes of the element which have an automation within the content of self Args: None Returns: None """ if self._transauto is not None and self._trans is not None: del self._trans [elt.reautomate() for elt in self._content.values()] #--------------------------------------------------------------------------# # conversion routines #--------------------------------------------------------------------------# def _to_pack(self): """Produces a list of tuples (type, val, bl) ready to be packed with pack_val() """ if not self.get_trans(): return self.get_alt()._to_pack() else: return [] def _from_char(self, char): """Dispatch the consumption of a Charpy intance to the selected element within the content """ if not self.get_trans(): # truncate char if length automation is set if self._blauto is not None: char_lb = char._len_bit char._len_bit = char._cur + self._blauto() if char._len_bit > char_lb: raise(EltErr('{0} [_from_char]: bit length overflow'.format(self._name))) self.get_alt()._from_char(char) # in case of length automation, set the original length back if self._blauto is not None: char._len_bit = char_lb #--------------------------------------------------------------------------# # copy / cloning routines #--------------------------------------------------------------------------# def get_attrs(self): """Returns the dictionnary of universal attributes of self and the elements within its content Args: None Returns: attrs (dict) : dictionnary of attributes """ return {'name' : self._name, 'desc' : self._desc, 'hier' : self._hier, 'trans' : self._trans, 'content': {sv: elt.get_attrs() for (sv, elt) in self._content.items()}} def get_attrs_all(self): """Returns the dictionnary of all attributes of self and the elements within its content Args: None Returns: attrs (dict) : dictionnary of attributes """ return {'env' : self._env, 'name' : self._name, 'desc' : self._desc, 'hier' : self._hier, 'trans' : self._trans, 'transauto': self._transauto, 'sel' : self._sel, 'content' : {sv: elt.get_attrs_all() for (sv, elt) in self._content.items()}} def set_attrs(self, **kw): """Updates the attributes of self and the elements within its content Args: kw (dict): dict of attributes and associated values attributes can be name, desc, hier, trans, bl, val and sel Returns: None """ if 'name' in kw and isinstance(kw['name'], str): self._name = kw['name'] if 'desc' in kw and isinstance(kw['desc'], str) and kw['desc'] != self.__class__._desc: self._desc = str(kw['desc']) if 'hier' in kw and kw['hier'] != self.__class__._hier: self._hier = kw['hier'] if 'trans' in kw and kw['trans'] != self.__class__._trans: self._trans = kw['trans'] # if self._SAFE_STAT: self._chk_hier() self._chk_trans() # if 'sel' in kw: self.set_sel(kw['sel']) if 'bl' in kw: self.set_bl(kw['bl']) if 'val' in kw: self.set_val(kw['val']) def clone(self): """Produces an independent clone of self Args: None Returns: clone (self.__class__ instance) """ kw = { 'GEN': self._GEN, 'DEFAULT': self.DEFAULT.clone(), 'sel': self._sel } if self._desc != self.__class__._desc: kw['desc'] = self._desc if self._hier != self.__class__._hier: kw['hier'] = self._hier if self._trans != self.__class__._trans: kw['trans'] = self._trans clone = self.__class__(self._name, **kw) clone.insert(self.get_sel(), self.get_alt().clone()) return clone #--------------------------------------------------------------------------# # Python list / dict methods emulation #--------------------------------------------------------------------------# def update(self, elt_alt): """Updates the dict of alternatives element with the content of `elt_alt' Args: elt_alt (dict of {selection value: element}) : dict of alternatives to update self._content with Returns: None Raises: EltErr : if self._SAFE_STAT is enabled and one of the alternative has an invalid type """ for sv, elt in elt_alt.items(): if self._SAFE_STAT and not isinstance(elt, Element): raise(EltErr('{0} [update]: alternative arg type is {1}, expecting element'\ .format(self._name, type(elt).__name__))) self._content[sv] = elt elt.set_env(self.get_env()) def insert(self, sv, elt): """Insert the element `elt' with the given selection value `sv' in the content of self Args: sv : selectio value elt (element) : element to be inserted Returns: None Raises: EltErr : if self._SAFE_STAT is enabled and the type of `elt' is not Element, or if selection value is invalid """ if self._SAFE_STAT and not isinstance(elt, Element): raise(EltErr('{0} [insert]: arg type is {1}, expecting element'\ .format(self._name, type(elt).__name__))) try: self._content[sv] = elt except Exception: raise(EltErr('{0} [insert]: selection value is invalid, {1}'.format(self._name, sv))) else: elt.set_env(self.get_env()) def index(self, elt): """Provide the selection value of the element `elt' within the content of self Args: elt (element) : element to be looked-up in the alt Returns: ind (int) : selection value of the element within the envelope Raises: EltErr : element `elt' is not in the content """ for sv, alt in self._content.items(): if elt == alt: return sv raise(EltErr('{0} [index]: non existent element, {1}'.format(self._name, elt))) def clear(self): """Clears the content of self Args: None Returns: None """ self._content.clear() # subscript methods passthrough def __getitem__(self, key): return self.get_alt().__getitem__(key) def __setitem__(self, key, val): return self.get_alt().__setitem__(key, val) def __delitem(self, key): return self.get_alt().__delitem__(key) #--------------------------------------------------------------------------# # representation routines #--------------------------------------------------------------------------# def repr(self): if self.REPR_EXPL: sv, alt = self.get_sel(), self.get_alt() # element transparency if self.get_trans(): trans = ' [transparent]' else: trans = '' # additional description if self._desc: desc = ' [%s]' % self._desc else: desc = '' # return '<%s%s%s : %r -> %s' % (self._name, desc, trans, sv, alt.repr()[1:]) else: return self.get_alt().repr() def show(self): if self.REPR_EXPL: sv, alt = self.get_sel(), self.get_alt() # element transparency if self.get_trans(): trans = ' [transparent]' else: trans = '' # additional description if self._desc: desc = ' [%s]' % self._desc else: desc = '' # alts = alt.show() if alts.lstrip()[:4] == '### ': # when the alternative is a constructed element return alts.replace('### ', '### %s%s%s : %r -> ' % (self._name, desc, trans, sv), 1) else: # when the alternative is a base element spaces = self.get_hier_abs() * ' ' return '%s### %s%s%s : %r ###\n %s' % (spaces, self._name, desc, trans, sv, alts) else: alt = self.get_alt() _hier = alt._hier alt._hier = self._hier s = alt.show() alt._hier = _hier return s #--------------------------------------------------------------------------# # Python built-ins override #--------------------------------------------------------------------------# __call__ = get_val __repr__ = repr #if python_implementation != 'PyPy': # PyPy iterator implementation lead to an infinite loop # __iter__() calls __len__(), but here, get_bl() calls __iter__() # __len__ = get_bl #--------------------------------------------------------------------------# # json interface #--------------------------------------------------------------------------# if _with_json: def _from_jval(self, val): self.get_alt()._from_jval_wrap(val) def _to_jval(self): return self.get_alt()._to_jval_wrap()