exposed dummy module generation tools code

This commit is contained in:
Eric Froemling 2021-06-12 18:23:39 -05:00
parent b85f2ea0d1
commit 767cdbb4c8
No known key found for this signature in database
GPG Key ID: 89C93F0F8D6D5A98
5 changed files with 795 additions and 25 deletions

View File

@ -12,7 +12,7 @@ Ideally this should be a stub (.pyi) file, but we'd need
to make sure that it still works with all our tools
(mypy, pylint, pycharm).
NOTE: This file was autogenerated by gendummymodule; do not edit by hand.
NOTE: This file was autogenerated by batools.dummymodule; do not edit by hand.
"""
# I'm sorry Pylint. I know this file saddens you. Be strong.

757
tools/batools/dummymodule.py Executable file
View File

@ -0,0 +1,757 @@
# Released under the MIT License. See LICENSE for details.
#
"""Generates a dummy _ba.py based on the game's real internal _ba module.
This allows us to use code introspection tools such as pylint from outside
the game, and also allows external scripts to import game scripts successfully
(though with limited functionality)
"""
from __future__ import annotations
import os
import sys
import textwrap
import subprocess
from typing import TYPE_CHECKING
from efro.error import CleanError
from efro.terminal import Clr
from efrotools import get_files_hash
if TYPE_CHECKING:
from types import ModuleType
from typing import Sequence, Any, Tuple, List
from batools.gendocs import AttributeInfo
def _get_varying_func_info(sig_in: str) -> Tuple[str, str]:
"""Return overloaded signatures and return statements for varying funcs."""
returns = 'return None'
if sig_in == ('getdelegate(self, type: Type,'
' doraise: bool = False) -> <varies>'):
sig = ('# Show that ur return type varies based on "doraise" value:\n'
'@overload\n'
'def getdelegate(self, type: Type[_T],'
' doraise: Literal[False] = False) -> Optional[_T]:\n'
' ...\n'
'\n'
'@overload\n'
'def getdelegate(self, type: Type[_T],'
' doraise: Literal[True]) -> _T:\n'
' ...\n'
'\n'
'def getdelegate(self, type: Any,'
' doraise: bool = False) -> Any:\n')
elif sig_in == ('getinputdevice(name: str, unique_id:'
' str, doraise: bool = True) -> <varies>'):
sig = ('# Show that our return type varies based on "doraise" value:\n'
'@overload\n'
'def getinputdevice(name: str, unique_id: str,'
' doraise: Literal[True] = True) -> ba.InputDevice:\n'
' ...\n'
'\n'
'@overload\n'
'def getinputdevice(name: str, unique_id: str,'
' doraise: Literal[False]) -> Optional[ba.InputDevice]:\n'
' ...\n'
'\n'
'def getinputdevice(name: str, unique_id: str,'
' doraise: bool=True) -> Any:')
elif sig_in == ('time(timetype: ba.TimeType = TimeType.SIM,'
' timeformat: ba.TimeFormat = TimeFormat.SECONDS)'
' -> <varies>'):
sig = (
'# Overloads to return a type based on requested format.\n'
'\n'
'@overload\n'
'def time(timetype: ba.TimeType = TimeType.SIM,\n'
' timeformat: Literal[TimeFormat.SECONDS]'
' = TimeFormat.SECONDS) -> float:\n'
' ...\n'
'\n'
'# This "*"'
' keyword-only hack lets us accept 1 arg'
' (timeformat=MILLISECS) forms.\n'
'@overload\n'
'def time(timetype: ba.TimeType = TimeType.SIM, *,\n'
' timeformat: Literal[TimeFormat.MILLISECONDS]) -> int:\n'
' ...\n'
'\n'
'@overload\n'
'def time(timetype: ba.TimeType,\n'
' timeformat: Literal[TimeFormat.MILLISECONDS]) -> int:\n'
' ...\n'
'\n'
'\n'
'def time(timetype: ba.TimeType = TimeType.SIM,\n'
' timeformat: ba.TimeFormat = TimeFormat.SECONDS)'
' -> Any:\n')
elif sig_in == 'getactivity(doraise: bool = True) -> <varies>':
sig = (
'# Show that our return type varies based on "doraise" value:\n'
'@overload\n'
'def getactivity(doraise: Literal[True] = True) -> ba.Activity:\n'
' ...\n'
'\n'
'\n'
'@overload\n'
'def getactivity(doraise: Literal[False])'
' -> Optional[ba.Activity]:\n'
' ...\n'
'\n'
'\n'
'def getactivity(doraise: bool = True) -> Optional[ba.Activity]:')
elif sig_in == 'getsession(doraise: bool = True) -> <varies>':
sig = ('# Show that our return type varies based on "doraise" value:\n'
'@overload\n'
'def getsession(doraise: Literal[True] = True) -> ba.Session:\n'
' ...\n'
'\n'
'\n'
'@overload\n'
'def getsession(doraise: Literal[False])'
' -> Optional[ba.Session]:\n'
' ...\n'
'\n'
'\n'
'def getsession(doraise: bool = True) -> Optional[ba.Session]:')
else:
raise RuntimeError(
f'Unimplemented varying func: {Clr.RED}{sig_in}{Clr.RST}')
return sig, returns
def _writefuncs(parent: Any, funcnames: Sequence[str], indent: int,
spacing: int, as_method: bool) -> str:
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
# pylint: disable=too-many-locals
out = ''
spcstr = '\n' * spacing
indstr = ' ' * indent
for funcname in funcnames:
# Skip some that are not in public builds.
if funcname in {'master_hash_dump'}:
continue
func = getattr(parent, funcname)
docstr = func.__doc__
# We expect an empty line and take everything before that to be
# the function signature.
if '\n\n' not in docstr:
raise Exception(f'docstr missing empty line: {func}')
sig = docstr.split('\n\n')[0].replace('\n', ' ').strip()
# Sanity check - make sure name is in the sig.
if funcname + '(' not in sig:
raise Exception(f'func name not found in sig for {funcname}')
# If these are methods, add self.
if as_method:
if funcname + '()' in sig:
sig = sig.replace(funcname + '()', funcname + '(self)')
else:
sig = sig.replace(funcname + '(', funcname + '(self, ')
# We expect sig to have a -> denoting return type.
if ' -> ' not in sig:
raise Exception(f'no "->" found in docstr for {funcname}')
returns = sig.split('->')[-1].strip()
# Some functions don't have simple signatures; we need to hard-code
# those here with overloads and whatnot.
if '<varies>' in sig:
overloadsigs, returnstr = _get_varying_func_info(sig)
defsline = textwrap.indent(overloadsigs, indstr)
else:
defsline = f'{indstr}def {sig}:\n'
# Types can be strings for forward-declaration cases.
if ((returns[0] == "'" and returns[-1] == "'")
or (returns[0] == '"' and returns[-1] == '"')):
returns = returns[1:-1]
if returns == 'None':
returnstr = 'return None'
elif returns == 'ba.Lstr':
returnstr = ('import ba # pylint: disable=cyclic-import\n'
"return ba.Lstr(value='')")
elif returns in ('ba.Activity', 'Optional[ba.Activity]'):
returnstr = (
'import ba # pylint: disable=cyclic-import\nreturn ' +
'ba.Activity(settings={})')
elif returns in ('ba.Session', 'Optional[ba.Session]'):
returnstr = (
'import ba # pylint: disable=cyclic-import\nreturn ' +
'ba.Session([])')
elif returns == 'Optional[ba.SessionPlayer]':
returnstr = ('import ba # pylint: disable=cyclic-import\n'
'return ba.SessionPlayer()')
elif returns == 'Optional[ba.Player]':
returnstr = ('import ba # pylint: disable=cyclic-import\n'
'return ba.Player()')
elif returns.startswith('ba.'):
# We cant import ba at module level so let's
# do it within funcs as needed.
returnstr = (
'import ba # pylint: disable=cyclic-import\nreturn ' +
returns + '()')
# we'll also have to go without a sig
# (could use the equivalent _ba class perhaps)
# sig = sig.split('->')[0]
elif returns in ['object', 'Any']:
# We use 'object' when we mean "can vary"
# don't want pylint making assumptions in this case.
returnstr = 'return _uninferrable()'
elif returns == 'Tuple[float, float]':
returnstr = 'return (0.0, 0.0)'
elif returns == 'Optional[str]':
returnstr = "return ''"
elif returns == 'Tuple[float, float, float, float]':
returnstr = 'return (0.0, 0.0, 0.0, 0.0)'
elif returns == 'Optional[ba.Widget]':
returnstr = 'return Widget()'
elif returns == 'Optional[ba.InputDevice]':
returnstr = 'return InputDevice()'
elif returns == 'List[ba.Widget]':
returnstr = 'return [Widget()]'
elif returns == 'Tuple[float, ...]':
returnstr = 'return (0.0, 0.0, 0.0)'
elif returns == 'List[str]':
returnstr = "return ['blah', 'blah2']"
elif returns == 'Union[float, int]':
returnstr = 'return 0.0'
elif returns == 'Dict[str, Any]':
returnstr = "return {'foo': 'bar'}"
elif returns in ('Optional[Tuple[int, int]]', 'Tuple[int, int]'):
returnstr = 'return (0, 0)'
elif returns == 'List[Dict[str, Any]]':
returnstr = "return [{'foo': 'bar'}]"
elif returns in [
'session.Session', 'team.Team', '_app.App',
'appconfig.AppConfig'
]:
returnstr = ('from ba import ' + returns.split('.')[0] +
'; return ' + returns + '()')
elif returns in [
'bool', 'str', 'int', 'list', 'dict', 'tuple', 'float',
'SessionData', 'ActivityData', 'Player', 'SessionPlayer',
'InputDevice', 'Sound', 'Texture', 'Model', 'CollideModel',
'team.Team', 'Vec3', 'Widget', 'Node'
]:
returnstr = 'return ' + returns + '()'
else:
raise Exception(
f'unknown returns value: {returns} for {funcname}')
returnspc = indstr + ' '
returnstr = ('\n' + returnspc).join(returnstr.strip().splitlines())
docstr_out = _formatdoc(docstr, indent + 4)
out += spcstr + defsline + docstr_out + f'{returnspc}{returnstr}\n'
return out
def _special_class_cases(classname: str) -> str:
out = ''
# Special case: define a fallback attr getter with a random
# return type in cases where our class handles attrs itself.
if classname in ['Vec3']:
out += ('\n'
' # pylint: disable=function-redefined\n'
'\n'
' @overload\n'
' def __init__(self) -> None:\n'
' pass\n'
'\n'
' @overload\n'
' def __init__(self, value: float):\n'
' pass\n'
'\n'
' @overload\n'
' def __init__(self, values: Sequence[float]):\n'
' pass\n'
'\n'
' @overload\n'
' def __init__(self, x: float, y: float, z: float):\n'
' pass\n'
'\n'
' def __init__(self, *args: Any, **kwds: Any):\n'
' pass\n'
'\n'
' def __add__(self, other: Vec3) -> Vec3:\n'
' return self\n'
'\n'
' def __sub__(self, other: Vec3) -> Vec3:\n'
' return self\n'
'\n'
' @overload\n'
' def __mul__(self, other: float) -> Vec3:\n'
' return self\n'
'\n'
' @overload\n'
' def __mul__(self, other: Sequence[float]) -> Vec3:\n'
' return self\n'
'\n'
' def __mul__(self, other: Any) -> Any:\n'
' return self\n'
'\n'
' @overload\n'
' def __rmul__(self, other: float) -> Vec3:\n'
' return self\n'
'\n'
' @overload\n'
' def __rmul__(self, other: Sequence[float]) -> Vec3:\n'
' return self\n'
'\n'
' def __rmul__(self, other: Any) -> Any:\n'
' return self\n'
'\n'
' # (for index access)\n'
' def __getitem__(self, typeargs: Any) -> Any:\n'
' return 0.0\n'
'\n'
' def __len__(self) -> int:\n'
' return 3\n'
'\n'
' # (for iterator access)\n'
' def __iter__(self) -> Any:\n'
' return self\n'
'\n'
' def __next__(self) -> float:\n'
' return 0.0\n'
'\n'
' def __neg__(self) -> Vec3:\n'
' return self\n'
'\n'
' def __setitem__(self, index: int, val: float) -> None:\n'
' pass\n')
if classname in ['Node']:
out += (
'\n'
' # Note attributes:\n'
' # NOTE: I\'m just adding *all* possible node attrs here\n'
' # now now since we have a single ba.Node type; in the\n'
' # future I hope to create proper individual classes\n'
' # corresponding to different node types with correct\n'
' # attributes per node-type.\n'
' color: Sequence[float] = (0.0, 0.0, 0.0)\n'
' size: Sequence[float] = (0.0, 0.0, 0.0)\n'
' position: Sequence[float] = (0.0, 0.0, 0.0)\n'
' position_center: Sequence[float] = (0.0, 0.0, 0.0)\n'
' position_forward: Sequence[float] = (0.0, 0.0, 0.0)\n'
' punch_position: Sequence[float] = (0.0, 0.0, 0.0)\n'
' punch_velocity: Sequence[float] = (0.0, 0.0, 0.0)\n'
' velocity: Sequence[float] = (0.0, 0.0, 0.0)\n'
' name_color: Sequence[float] = (0.0, 0.0, 0.0)\n'
' tint_color: Sequence[float] = (0.0, 0.0, 0.0)\n'
' tint2_color: Sequence[float] = (0.0, 0.0, 0.0)\n'
" text: Union[ba.Lstr, str] = ''\n"
' texture: Optional[ba.Texture] = None\n'
' tint_texture: Optional[ba.Texture] = None\n'
' times: Sequence[int] = (1,2,3,4,5)\n'
' values: Sequence[float] = (1.0, 2.0, 3.0, 4.0)\n'
' offset: float = 0.0\n'
' input0: float = 0.0\n'
' input1: float = 0.0\n'
' input2: float = 0.0\n'
' input3: float = 0.0\n'
' flashing: bool = False\n'
' scale: Union[float, Sequence[float]] = 0.0\n' # FIXME
' opacity: float = 0.0\n'
' loop: bool = False\n'
' time1: int = 0\n'
' time2: int = 0\n'
' timemax: int = 0\n'
' client_only: bool = False\n'
' materials: Sequence[Material] = ()\n'
' roller_materials: Sequence[Material] = ()\n'
" name: str = ''\n"
' punch_materials: Sequence[ba.Material] = ()\n'
' pickup_materials: Sequence[ba.Material] = ()\n'
' extras_material: Sequence[ba.Material] = ()\n'
' rotate: float = 0.0\n'
' hold_node: Optional[ba.Node] = None\n'
' hold_body: int = 0\n'
' host_only: bool = False\n'
' premultiplied: bool = False\n'
' source_player: Optional[ba.Player] = None\n'
' model_opaque: Optional[ba.Model] = None\n'
' model_transparent: Optional[ba.Model] = None\n'
' damage_smoothed: float = 0.0\n'
' gravity_scale: float = 1.0\n'
' punch_power: float = 0.0\n'
' punch_momentum_linear: Sequence[float] = '
'(0.0, 0.0, 0.0)\n'
' punch_momentum_angular: float = 0.0\n'
' rate: int = 0\n'
' vr_depth: float = 0.0\n'
' is_area_of_interest: bool = False\n'
' jump_pressed: bool = False\n'
' pickup_pressed: bool = False\n'
' punch_pressed: bool = False\n'
' bomb_pressed: bool = False\n'
' fly_pressed: bool = False\n'
' hold_position_pressed: bool = False\n'
' knockout: float = 0.0\n'
' invincible: bool = False\n'
' stick_to_owner: bool = False\n'
' damage: int = 0\n'
' run: float = 0.0\n'
' move_up_down: float = 0.0\n'
' move_left_right: float = 0.0\n'
' curse_death_time: int = 0\n'
' boxing_gloves: bool = False\n'
' use_fixed_vr_overlay: bool = False\n'
' allow_kick_idle_players: bool = False\n'
' music_continuous: bool = False\n'
' music_count: int = 0\n'
' hurt: float = 0.0\n'
' always_show_health_bar: bool = False\n'
' mini_billboard_1_texture: Optional[ba.Texture] = None\n'
' mini_billboard_1_start_time: int = 0\n'
' mini_billboard_1_end_time: int = 0\n'
' mini_billboard_2_texture: Optional[ba.Texture] = None\n'
' mini_billboard_2_start_time: int = 0\n'
' mini_billboard_2_end_time: int = 0\n'
' mini_billboard_3_texture: Optional[ba.Texture] = None\n'
' mini_billboard_3_start_time: int = 0\n'
' mini_billboard_3_end_time: int = 0\n'
' boxing_gloves_flashing: bool = False\n'
' dead: bool = False\n'
' floor_reflection: bool = False\n'
' debris_friction: float = 0.0\n'
' debris_kill_height: float = 0.0\n'
' vr_near_clip: float = 0.0\n'
' shadow_ortho: bool = False\n'
' happy_thoughts_mode: bool = False\n'
' shadow_offset: Sequence[float] = (0.0, 0.0)\n'
' paused: bool = False\n'
' time: int = 0\n'
' ambient_color: Sequence[float] = (1.0, 1.0, 1.0)\n'
" camera_mode: str = 'rotate'\n"
' frozen: bool = False\n'
' area_of_interest_bounds: Sequence[float]'
' = (-1, -1, -1, 1, 1, 1)\n'
' shadow_range: Sequence[float] = (0, 0, 0, 0)\n'
" counter_text: str = ''\n"
' counter_texture: Optional[ba.Texture] = None\n'
' shattered: int = 0\n'
' billboard_texture: Optional[ba.Texture] = None\n'
' billboard_cross_out: bool = False\n'
' billboard_opacity: float = 0.0\n'
' slow_motion: bool = False\n'
" music: str = ''\n"
' vr_camera_offset: Sequence[float] = (0.0, 0.0, 0.0)\n'
' vr_overlay_center: Sequence[float] = (0.0, 0.0, 0.0)\n'
' vr_overlay_center_enabled: bool = False\n'
' vignette_outer: Sequence[float] = (0.0, 0.0)\n'
' vignette_inner: Sequence[float] = (0.0, 0.0)\n'
' tint: Sequence[float] = (1.0, 1.0, 1.0)\n')
# Special case: need to be able to use the 'with' statement
# on some classes.
if classname in ['Context']:
out += ('\n'
' def __enter__(self) -> None:\n'
' """Support for "with" statement."""\n'
' pass\n'
'\n'
' def __exit__(self, exc_type: Any, exc_value: Any, '
'traceback: Any) -> Any:\n'
' """Support for "with" statement."""\n'
' pass\n')
return out
def _formatdoc(docstr: str, indent: int) -> str:
out = ''
indentstr = indent * ' '
docslines = docstr.splitlines()
if len(docslines) == 1:
out += '\n' + indentstr + '"""' + docslines[0] + '"""\n'
else:
for i, line in enumerate(docslines):
if i != 0 and line != '':
docslines[i] = indentstr + line
out += ('\n' + indentstr + '"""' + '\n'.join(docslines) + '\n' +
indentstr + '"""\n')
return out
def _writeclasses(module: ModuleType, classnames: Sequence[str]) -> str:
# pylint: disable=too-many-branches
import types
from batools.gendocs import parse_docs_attrs
out = ''
for classname in classnames:
cls = getattr(module, classname)
if cls is None:
raise Exception('unexpected')
out += '\n' '\n'
# Special case:
if classname == 'Vec3':
out += f'class {classname}(Sequence[float]):\n'
else:
out += f'class {classname}:\n'
docstr = cls.__doc__
out += _formatdoc(docstr, 4)
# Create a public constructor if it has one.
# If the first docs line appears to be a function signature
# and not category or a usage statement ending with a period,
# assume it has a public constructor.
has_constructor = False
if ('category:' not in docstr.splitlines()[0].lower()
and not docstr.splitlines()[0].endswith('.')
and docstr != '(internal)'):
# Ok.. looks like the first line is a signature.
# Make sure we've got a signature followed by a blank line.
if '\n\n' not in docstr:
raise Exception(
f'Constructor docstr missing empty line for {cls}.')
sig = docstr.split('\n\n')[0].replace('\n', ' ').strip()
# Sanity check - make sure name is in the sig.
if classname + '(' not in sig:
raise Exception(
f'Class name not found in constructor sig for {cls}.')
sig = sig.replace(classname + '(', '__init__(self, ')
out += ' def ' + sig + ':\n pass\n'
has_constructor = True
# Scan its doc-string for attribute info; drop in typed
# declarations for any that we find.
attrs: List[AttributeInfo] = []
parse_docs_attrs(attrs, docstr)
if attrs:
for attr in attrs:
if attr.attr_type is not None:
out += f' {attr.name}: {attr.attr_type}\n'
else:
raise Exception(f'Found untyped attr in'
f' {classname} docs: {attr.name}')
# Special cases such as attributes we add.
out += _special_class_cases(classname)
# Print its methods.
funcnames = []
for entry in (e for e in dir(cls) if not e.startswith('__')):
if isinstance(getattr(cls, entry), types.MethodDescriptorType):
funcnames.append(entry)
else:
raise Exception(f'Unhandled obj {entry} in {cls}')
funcnames.sort()
functxt = _writefuncs(cls,
funcnames,
indent=4,
spacing=1,
as_method=True)
if functxt == '' and not has_constructor:
out += ' pass\n'
else:
out += functxt
return out
def generate(sources_hash: str, outfilename: str) -> None:
"""Run the actual generation from within the game."""
import _ba as module
from efrotools import get_public_license, PYVER
import types
funcnames = []
classnames = []
for entry in (e for e in dir(module) if not e.startswith('__')):
if isinstance(getattr(module, entry), types.BuiltinFunctionType):
funcnames.append(entry)
elif isinstance(getattr(module, entry), type):
classnames.append(entry)
elif entry == 'app':
# Ignore _ba.app.
continue
else:
raise Exception(f'found unknown obj {entry}')
funcnames.sort()
classnames.sort()
out = (get_public_license('python')
+ '\n'
'#\n'
'"""A dummy stub module for the real _ba.\n'
'\n'
'The real _ba is a compiled extension module and only available\n'
'in the live game. This dummy module allows Pylint/Mypy/etc. to\n'
'function reasonably well outside of the game.\n'
'\n'
'Make sure this file is never included in an actual game distro!\n'
'\n'
'Ideally this should be a stub (.pyi) file, but we\'d need\n'
'to make sure that it still works with all our tools\n'
'(mypy, pylint, pycharm).\n'
'\n'
'NOTE: This file was autogenerated by ' + __name__ + '; '
'do not edit by hand.\n'
'"""\n'
'\n'
# '# (hash we can use to see if this file is out of date)\n'
# '# SOURCES_HASH='+sources_hash+'\n'
# '\n'
'# I\'m sorry Pylint. I know this file saddens you. Be strong.\n'
'# pylint: disable=useless-suppression\n'
'# pylint: disable=unnecessary-pass\n'
'# pylint: disable=unused-argument\n'
'# pylint: disable=missing-docstring\n'
'# pylint: disable=too-many-locals\n'
'# pylint: disable=redefined-builtin\n'
'# pylint: disable=too-many-lines\n'
'# pylint: disable=redefined-outer-name\n'
'# pylint: disable=invalid-name\n'
'# pylint: disable=no-self-use\n'
'# pylint: disable=no-value-for-parameter\n'
'\n'
'from __future__ import annotations\n'
'\n'
'from typing import TYPE_CHECKING, overload, Sequence, TypeVar\n'
'\n'
'from ba._enums import TimeFormat, TimeType\n'
'\n'
'if TYPE_CHECKING:\n'
' from typing import (Any, Dict, Callable, Tuple, '
' List, Optional, Union, List, Type, Literal)\n'
' from ba._app import App\n'
' import ba\n'
'\n'
'\n'
"_T = TypeVar('_T')\n"
'\n'
'app: App\n'
'\n'
'def _uninferrable() -> Any:\n'
' """Get an "Any" in mypy and "uninferrable" in Pylint."""\n'
' # pylint: disable=undefined-variable\n'
' return _not_a_real_variable # type: ignore'
'\n'
'\n'
) # yapf: disable
out += _writeclasses(module, classnames)
out += _writefuncs(module, funcnames, indent=0, spacing=2, as_method=False)
outhashpath = os.path.join(os.path.dirname(outfilename),
'._ba_sources_hash')
with open(outfilename, 'w') as outfile:
outfile.write(out)
with open(outhashpath, 'w') as outfile:
outfile.write(sources_hash)
# Lastly, format it.
subprocess.run([f'python{PYVER}', '-m', 'yapf', '--in-place', outfilename],
check=True)
def _dummy_module_dirty() -> Tuple[bool, str]:
"""Test hashes on the dummy module to see if it needs updates."""
# Let's generate a hash from all sources under the python source dir.
pysources = []
exts = ['.cc', '.c', '.h']
for root, _dirs, files in os.walk('src/ballistica/python'):
for fname in files:
if any(fname.endswith(ext) for ext in exts):
pysources.append(os.path.join(root, fname))
# Also lets add this script so we re-create when it changes.
pysources.append(__file__)
outpath = 'assets/src/ba_data/python/._ba_sources_hash'
if not os.path.exists(outpath):
existing_hash = ''
else:
with open(outpath) as infile:
existing_hash = infile.read()
pysources.sort()
# Note: returning plain integers instead of hex so linters
# don't see words and give spelling errors.
pysources_hash = get_files_hash(pysources, int_only=True)
dirty = existing_hash != pysources_hash
return dirty, pysources_hash
def update(projroot: str, check: bool, force: bool) -> None:
"""Update the dummy module as needed."""
toolsdir = os.path.abspath(os.path.join(projroot, 'tools'))
outfilename = os.path.abspath(
os.path.join(projroot, 'assets/src/ba_data/python/_ba.py'))
# Make sure we're running from the project root dir.
os.chdir(projroot)
# Force makes no sense in check mode.
if force and check:
raise Exception('cannot specify both force and check mode')
dirty, sources_hash = _dummy_module_dirty()
if dirty:
if check:
print(f'{Clr.RED}ERROR: dummy _ba module'
f' is out of date.{Clr.RST}')
sys.exit(255)
elif not force:
# Dummy module is clean and force is off; we're done here.
print('Dummy module _ba.py is up to date.')
sys.exit(0)
print(f'{Clr.MAG}Updating _ba.py Dummy Module...{Clr.RST}')
# Let's build the cmake version; no sandboxing issues to contend with
# there. Also going with the headless build; will need to revisit if
# there's ever any functionality not available in that build.
subprocess.run(['make', 'cmake-server-build'], check=True)
# Launch ballistica and exec ourself from within it.
print('Launching ballisticacore to generate dummy-module...')
try:
subprocess.run(
[
'./ballisticacore',
'-exec',
f'try:\n'
f' import sys\n'
f' sys.path.append("{toolsdir}")\n'
f' from batools import dummymodule\n'
f' dummymodule.generate(sources_hash="{sources_hash}",\n'
f' outfilename="{outfilename}")\n'
f' ba.quit()\n'
f'except Exception as exc:\n'
f' import sys\n'
f' import traceback\n'
f' print("ERROR GENERATING DUMMY MODULE")\n'
f' traceback.print_exc()\n'
f' sys.exit(255)\n',
],
cwd='build/cmake/server-debug/dist',
check=True,
)
except Exception as exc2:
# Keep our error simple here; we want focus to be on what went
# wrong withing BallisticaCore.
raise CleanError(
'BallisticaCore dummy-module generation failed.') from exc2
print('Dummy-module generation complete.')

View File

@ -901,3 +901,11 @@ 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)
def update_dummy_module() -> None:
"""Update our _ba dummy module."""
from batools.dummymodule import update
update(projroot=str(PROJROOT),
check='--check' in sys.argv,
force='--force' in sys.argv)

View File

@ -163,29 +163,6 @@ class Updater:
self._internal_source_dirs = set(sources)
return self._internal_source_dirs
def _update_dummy_module(self) -> None:
# Update our dummy _ba module.
# We need to do this near the end because it may run the cmake build
# so its success may depend on the cmake build files having already
# been updated.
if os.path.exists('tools/gendummymodule.py'):
if os.system('tools/gendummymodule.py' + self._checkarg) != 0:
print(
f'{Clr.RED}Error checking/updating dummy module.{Clr.RST}')
sys.exit(255)
def _update_docs_md(self) -> None:
# Update our docs/*.md files.
# We need to do this near the end because it may run the cmake build
# so its success may depend on the cmake build files having already
# been updated.
try:
subprocess.run(['tools/pcommand', 'update_docs_md'] +
self._checkarglist,
check=True)
except Exception as exc:
raise CleanError('Error checking/updating docs') from exc
def _update_prereqs(self) -> None:
# This will update our prereqs which may include compile-commands
@ -701,3 +678,31 @@ class Updater:
except Exception as exc:
raise CleanError(
'Error checking/updating python enums module.') from exc
def _update_dummy_module(self) -> None:
# Update our dummy _ba module.
# Note: This should happen near the end because it may run the cmake
# build so its success may depend on the cmake build files having
# already been updated.
# FIXME: should support running this in public too.
if not self._public:
try:
subprocess.run(['tools/pcommand', 'update_dummy_module'] +
self._checkarglist,
check=True)
except Exception as exc:
raise CleanError(
'Error checking/updating dummy module.') from exc
def _update_docs_md(self) -> None:
# Update our docs/*.md files.
# We need to do this near the end because it may run the cmake build
# so its success may depend on the cmake build files having already
# been updated.
try:
subprocess.run(['tools/pcommand', 'update_docs_md'] +
self._checkarglist,
check=True)
except Exception as exc:
raise CleanError('Error checking/updating docs') 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, update_python_enums_module)
xcode_build_path, update_python_enums_module, update_dummy_module)
# pylint: enable=unused-import
if TYPE_CHECKING: