consolidating enums generation code

This commit is contained in:
Eric Froemling 2021-06-12 17:33:21 -05:00
parent 1ae25e469b
commit b85f2ea0d1
No known key found for this signature in database
GPG Key ID: 89C93F0F8D6D5A98
7 changed files with 192 additions and 8 deletions

View File

@ -1341,6 +1341,7 @@
<w>megalint</w>
<w>memfunctions</w>
<w>menubar</w>
<w>metamakefile</w>
<w>metaprogramming</w>
<w>metascan</w>
<w>meteorshower</w>

View File

@ -1,5 +1,5 @@
# Released under the MIT License. See LICENSE for details.
"""Enums generated by tools/update_python_enums_module in ba-internal."""
"""Enum vals generated by batools.pythonenumsmodule; do not edit by hand."""
from enum import Enum

View File

@ -607,6 +607,7 @@
<w>memcpy</w>
<w>meshdata</w>
<w>messagebox</w>
<w>metamakefile</w>
<w>meth</w>
<w>mhbegin</w>
<w>mhend</w>

View File

@ -895,3 +895,9 @@ def xcode_build_path() -> None:
project_path=project_path,
configuration=configuration)
print(path)
def update_python_enums_module() -> None:
"""Update our procedurally generated python enums."""
from batools.pythonenumsmodule import update
update(projroot=str(PROJROOT), check='--check' in sys.argv)

View File

@ -0,0 +1,172 @@
#!/usr/bin/env python3.8
# Released under the MIT License. See LICENSE for details.
#
"""Procedurally regenerates our python enums module.
This scans core/types.h for tagged C++ enum types and generates corresponding
python enums for them.
"""
from __future__ import annotations
import os
import re
import sys
from typing import TYPE_CHECKING
from efro.terminal import Clr
from efrotools import get_public_license
if TYPE_CHECKING:
from typing import Optional, List, Tuple
OUTPUT_FILENAME = 'assets/src/ba_data/python/ba/_enums.py'
def camel_case_convert(name: str) -> str:
"""Convert camel-case text to upcase-with-underscores."""
str1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', str1).upper()
def _gen_enums() -> str:
out = ''
enum_lnums: List[int] = []
with open('src/ballistica/core/types.h') as infile:
lines = infile.read().splitlines()
# Tally up all places tagged for exporting python enums.
for i, line in enumerate(lines):
if '// BA_EXPORT_PYTHON_ENUM' in line:
enum_lnums.append(i + 1)
# Now export each of them.
for lnum in enum_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)
if len(doclines) > 1:
out += '\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('' if line == ' ' else line
for line in out.splitlines()) + '\n')
def _parse_name(lines: List[str], lnum: int) -> str:
bits = lines[lnum].split(' ')
if (len(bits) != 4 or bits[0] != 'enum' or bits[1] != 'class'
or bits[3] != '{'):
raise Exception(f'Unexpected format for enum on line {lnum + 1}.')
enum_name = bits[2]
return enum_name
def _parse_values(lines: List[str], lnum: int, lnumend: int, out: str) -> str:
val = 0
for i in range(lnum + 1, lnumend):
line = lines[i]
if line.strip().startswith('//'):
continue
# Strip off any trailing comment.
if '//' in line:
line = line.split('//')[0].strip()
# Strip off any trailing comma.
if line.endswith(','):
line = line[:-1].strip()
# If they're explicitly assigning a value, parse it.
if '=' in line:
splits = line.split()
if (len(splits) != 3 or splits[1] != '='
or not splits[2].isnumeric()):
raise RuntimeError(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()
if not name.startswith('k') or len(name) < 2:
raise RuntimeError(f"Expected name to start with 'k'; got {name}")
# We require kLast to be the final value
# (C++ requires this for bounds checking)
if i == lnumend - 1:
if name != 'kLast':
raise RuntimeError(
f'Expected last enum value of kLast; found {name}.')
continue
name = camel_case_convert(name[1:])
out += f' {name} = {val}\n'
val += 1
return out
def _find_enum_end(lines: List[str], lnum: int) -> int:
lnumend = lnum + 1
while True:
if lnumend > len(lines) - 1:
raise Exception(f'No end found for enum on line {lnum + 1}.')
if '};' in lines[lnumend]:
break
lnumend += 1
return lnumend
def _parse_doc_lines(lines: List[str], lnum: int) -> Tuple[List[str], int]:
# First parse the doc-string
doclines: List[str] = []
lnumorig = lnum
while True:
if lnum > len(lines) - 1:
raise Exception(
f'No end found for enum docstr line {lnumorig + 1}.')
if lines[lnum].startswith('enum class '):
break
if not lines[lnum].startswith('///'):
raise Exception(f'Invalid docstr at line {lnum + 1}.')
doclines.append(lines[lnum][4:])
lnum += 1
return doclines, lnum
def update(projroot: str, check: bool) -> None:
"""Main script entry point."""
# Operate out of root dist dir for consistency.
os.chdir(projroot)
fname = OUTPUT_FILENAME
existing: Optional[str]
try:
with open(fname, 'r') as infile:
existing = infile.read()
except Exception:
existing = None
out = (get_public_license('python') +
f'\n"""Enum vals generated by {__name__}; do not edit by hand."""'
f'\n\nfrom enum import Enum\n')
out += _gen_enums()
if out == existing:
print('Python enums module is up to date.')
else:
if check:
print(Clr.SRED + 'ERROR: file out of date: ' + fname + Clr.RST)
sys.exit(255)
print(Clr.SBLU + 'Generating: ' + fname + Clr.RST)
with open(fname, 'w') as outfile:
outfile.write(out)

View File

@ -691,9 +691,13 @@ class Updater:
'Error checking/updating resources Makefile.') from exc
def _update_python_enums_module(self) -> None:
if os.path.exists('tools/update_python_enums_module'):
if os.system('tools/update_python_enums_module' +
self._checkarg) != 0:
print(f'{Clr.RED}Error checking/updating'
f' python enums module.{Clr.RST}')
sys.exit(255)
# FIXME: should support running this in public too.
if not self._public:
try:
subprocess.run(
['tools/pcommand', 'update_python_enums_module'] +
self._checkarglist,
check=True)
except Exception as exc:
raise CleanError(
'Error checking/updating python enums module.') from exc

View File

@ -41,7 +41,7 @@ from batools.pcommand import (
cmake_prep_dir, gen_binding_code, gen_flat_data_code, wsl_path_to_win,
wsl_build_check_win_drive, win_ci_binary_build, genchangelog,
android_sdk_utils, update_resources_makefile, update_meta_makefile,
xcode_build_path)
xcode_build_path, update_python_enums_module)
# pylint: enable=unused-import
if TYPE_CHECKING: