# Released under the MIT License. See LICENSE for details.#"""Generate a Python module containing Enum classes from C++ code.Note that the general strategy moving forward is the opposite ofthis: to generate C++ code as needed from Python sources. That isgenerally a better direction to go since introspecting Python objectsor source code ast is more foolproof than the text based parsing weare doing here."""from__future__importannotationsimportreimportosfromtypingimportTYPE_CHECKINGfromefro.terminalimportClrfromefrotools.projectimportget_public_legal_noticeifTYPE_CHECKING:pass
[docs]defcamel_case_convert(name:str)->str:"""Convert camel-case text to upcase-with-underscores."""str1=re.sub('(.)([A-Z][a-z]+)',r'\1_\2',name)returnre.sub('([a-z0-9])([A-Z])',r'\1_\2',str1).upper()
def_gen_enums(infilename:str)->str:out=''enum_lnums:list[int]=[]withopen(infilename,encoding='utf-8')asinfile:lines=infile.read().splitlines()# Tally up all places tagged for exporting python enums.fori,lineinenumerate(lines):if'// BA_EXPORT_PYTHON_ENUM'inline:enum_lnums.append(i+1)# Now export each of them.forlnuminenum_lnums:doclines,lnum=_parse_doc_lines(lines,lnum)enum_name=_parse_name(lines,lnum)out+=f'\n\nclass {enum_name}(Enum):\n """'out+='\n '.join(doclines)iflen(doclines)>1:out+='\n """\n\n'else:out+='"""\n'lnumend=_find_enum_end(lines,lnum)out=_parse_values(lines,lnum,lnumend,out)# Clear lines with only spaces.return('\n'.join(''ifline==' 'elselineforlineinout.splitlines())+'\n')def_parse_name(lines:list[str],lnum:int)->str:bits=lines[lnum].split(' ')# Special case: allow for specifying underlying type.iflen(bits)==6andbits[3]==':'andbits[4]in{'uint8_t','uint16_t'}:bits=[bits[0],bits[1],bits[2],bits[5]]if(len(bits)!=4orbits[0]!='enum'orbits[1]!='class'orbits[3]!='{'):raiseRuntimeError(f'Unexpected format for enum on line {lnum+1}.')enum_name=bits[2]returnenum_namedef_parse_values(lines:list[str],lnum:int,lnumend:int,out:str)->str:val=0foriinrange(lnum+1,lnumend):line=lines[i]ifline.strip().startswith('//'):continue# Strip off any trailing comment.if'//'inline:line=line.split('//')[0].strip()# Strip off any trailing comma.ifline.endswith(','):line=line[:-1].strip()# If they're explicitly assigning a value, parse it.if'='inline:splits=line.split()if(len(splits)!=3orsplits[1]!='='ornotsplits[2].isnumeric()):raiseRuntimeError(f'Unable to parse enum value for: {line}')name=splits[0]val=int(splits[2])else:name=line# name = line.split(',')[0].split('//')[0].strip()ifnotname.startswith('k')orlen(name)<2:raiseRuntimeError(f"Expected name to start with 'k'; got {name}")# We require kLast to be the final value# (C++ requires this for bounds checking)ifi==lnumend-1:ifname!='kLast':raiseRuntimeError(f'Expected last enum value of kLast; found {name}.')continuename=camel_case_convert(name[1:])out+=f' {name} = {val}\n'val+=1returnoutdef_find_enum_end(lines:list[str],lnum:int)->int:lnumend=lnum+1whileTrue:iflnumend>len(lines)-1:raiseRuntimeError(f'No end found for enum on line {lnum+1}.')if'};'inlines[lnumend]:breaklnumend+=1returnlnumenddef_parse_doc_lines(lines:list[str],lnum:int)->tuple[list[str],int]:# First parse the doc-stringdoclines:list[str]=[]lnumorig=lnumwhileTrue:iflnum>len(lines)-1:raiseRuntimeError(f'No end found for enum docstr line {lnumorig+1}.')iflines[lnum].startswith('enum class '):breakifnotlines[lnum].startswith('///'):raiseRuntimeError(f'Invalid docstr at line {lnum+1}.')doclines.append(lines[lnum][4:])lnum+=1returndoclines,lnum
[docs]defgenerate(projroot:str,infilename:str,outfilename:str)->None:"""Main script entry point."""frombatools.projectimportproject_centric_pathout=(get_public_legal_notice('python')+f'\n"""Enum vals generated by {__name__}; do not edit by hand."""'f'\n\nfrom enum import Enum\n')out+=_gen_enums(infilename)path=project_centric_path(projroot=projroot,path=outfilename)print(f'Meta-building {Clr.BLD}{path}{Clr.RST}')os.makedirs(os.path.dirname(outfilename),exist_ok=True)withopen(outfilename,'w',encoding='utf-8')asoutfile:outfile.write(out)