# Released under the MIT License. See LICENSE for details.
#
"""Generate C++ enums from a module of Python ones.
The engine's enums that Python also needs are defined once, in Python
(``bacommon/nativeenums.py``), and this emits their C++ twins as
``shared/generated/enums.h``. Each C++ entry carries the Python
member's int value, followed by a ``kLast`` sentinel one past the
highest, which native code uses to range-check values arriving from
Python.
The source lives in bacommon rather than a feature set's own package
because every project needs the header, including ones built without
the base feature set. It is loaded by file path, so it should import
nothing but :mod:`enum`.
"""
from __future__ import annotations # Docs-generation hack.
import os
import inspect
import importlib.util
from enum import Enum
from efro.error import CleanError
[docs]
def cpp_name(member: Enum) -> str:
"""``PICK_UP_PRESS`` -> ``kPickUpPress``."""
return 'k' + ''.join(part.capitalize() for part in member.name.split('_'))
[docs]
def load_enums(src_path: str) -> list[type[Enum]]:
"""Return the enum classes a Python source file defines, in order."""
spec = importlib.util.spec_from_file_location('_cpp_enums_src', src_path)
if spec is None or spec.loader is None:
raise CleanError(f"Unable to load '{src_path}'.")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
enums = [
obj
for obj in vars(module).values()
if inspect.isclass(obj)
and issubclass(obj, Enum)
and obj.__module__ == module.__name__
]
if not enums:
raise CleanError(f"No enums found in '{src_path}'.")
for enum in enums:
_check_enum(enum)
return enums
def _check_enum(enum: type[Enum]) -> None:
"""Make sure an enum can be expressed as a uint8_t C++ one."""
prev = -1
for member in enum:
value = member.value
if not isinstance(value, int) or isinstance(value, bool):
raise CleanError(
f'{enum.__name__}.{member.name} has a non-int value.'
)
if value <= prev:
raise CleanError(
f'{enum.__name__}.{member.name} is out of order;'
' values must be non-negative and ascending.'
)
prev = value
# (kLast takes the slot after the highest value.)
if prev >= 255:
raise CleanError(f'{enum.__name__} has values too big for uint8_t.')
def _doc_lines(enum: type[Enum]) -> list[str]:
"""An enum's docstring as C++ doc-comment lines."""
doc = inspect.getdoc(enum) or ''
return [f'/// {line}'.rstrip() for line in doc.splitlines()]
[docs]
def generate_cpp(src_path: str, src_display_path: str) -> str:
"""Return the header's contents."""
guard = 'BALLISTICA_SHARED_GENERATED_ENUMS_H_'
out = [
'// Released under the MIT License. See LICENSE for details.',
'//',
'// AUTO-GENERATED by `tools/pcommand gen_cpp_enums`.',
'// DO NOT EDIT BY HAND.',
'//',
f'// Generated from {src_display_path}.',
'',
f'#ifndef {guard}',
f'#define {guard}',
'',
'#include <cstdint>',
'',
'namespace ballistica {',
'',
]
for enum in load_enums(src_path):
out += _doc_lines(enum)
out.append(f'enum class {enum.__name__} : uint8_t {{')
out += [f' {cpp_name(member)} = {member.value},' for member in enum]
out += [' kLast // Sentinel.', '};', '']
out += [
'} // namespace ballistica',
'',
f'#endif // {guard}',
'',
]
return '\n'.join(out)
[docs]
def write_cpp(src_path: str, out_path: str, projroot: str) -> None:
"""Write the header, leaving unchanged bytes alone."""
contents = generate_cpp(
src_path, os.path.relpath(os.path.abspath(src_path), projroot)
)
os.makedirs(os.path.dirname(out_path), exist_ok=True)
if os.path.exists(out_path):
with open(out_path, encoding='utf-8') as infile:
if infile.read() == contents:
# Leave the bytes alone (no spurious rebuilds
# downstream) but bump the mtime so make sees the
# target as newer than its sources.
os.utime(out_path, None)
return
with open(out_path, 'w', encoding='utf-8') as outfile:
outfile.write(contents)
# Docs-generation hack; import some stuff that we likely only forward-declared
# in our actual source code so that docs tools can find it.
from typing import (Coroutine, Any, Literal, Callable,
Generator, Awaitable, Sequence, Self, TypeIs)
import asyncio
from concurrent.futures import Future
from pathlib import Path
from enum import Enum