From b85f2ea0d143f01475ad0e3572abbe5399a58251 Mon Sep 17 00:00:00 2001 From: Eric Froemling Date: Sat, 12 Jun 2021 17:33:21 -0500 Subject: [PATCH] consolidating enums generation code --- .idea/dictionaries/ericf.xml | 1 + assets/src/ba_data/python/ba/_enums.py | 2 +- .../.idea/dictionaries/ericf.xml | 1 + tools/batools/pcommand.py | 6 + tools/batools/pythonenumsmodule.py | 172 ++++++++++++++++++ tools/batools/updateproject.py | 16 +- tools/pcommand | 2 +- 7 files changed, 192 insertions(+), 8 deletions(-) create mode 100755 tools/batools/pythonenumsmodule.py diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml index 94289c2c..8a4c77a9 100644 --- a/.idea/dictionaries/ericf.xml +++ b/.idea/dictionaries/ericf.xml @@ -1341,6 +1341,7 @@ megalint memfunctions menubar + metamakefile metaprogramming metascan meteorshower diff --git a/assets/src/ba_data/python/ba/_enums.py b/assets/src/ba_data/python/ba/_enums.py index 7940bcfd..ea937b50 100644 --- a/assets/src/ba_data/python/ba/_enums.py +++ b/assets/src/ba_data/python/ba/_enums.py @@ -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 diff --git a/ballisticacore-cmake/.idea/dictionaries/ericf.xml b/ballisticacore-cmake/.idea/dictionaries/ericf.xml index eb2adef3..584d9868 100644 --- a/ballisticacore-cmake/.idea/dictionaries/ericf.xml +++ b/ballisticacore-cmake/.idea/dictionaries/ericf.xml @@ -607,6 +607,7 @@ memcpy meshdata messagebox + metamakefile meth mhbegin mhend diff --git a/tools/batools/pcommand.py b/tools/batools/pcommand.py index 682859c4..3c1e55e8 100644 --- a/tools/batools/pcommand.py +++ b/tools/batools/pcommand.py @@ -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) diff --git a/tools/batools/pythonenumsmodule.py b/tools/batools/pythonenumsmodule.py new file mode 100755 index 00000000..c7aa690a --- /dev/null +++ b/tools/batools/pythonenumsmodule.py @@ -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) diff --git a/tools/batools/updateproject.py b/tools/batools/updateproject.py index 3f0795e0..1587541a 100755 --- a/tools/batools/updateproject.py +++ b/tools/batools/updateproject.py @@ -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 diff --git a/tools/pcommand b/tools/pcommand index 55b6e3f1..b99589e9 100755 --- a/tools/pcommand +++ b/tools/pcommand @@ -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: