mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-26 17:03:14 +08:00
exposed dummy module generation tools code
This commit is contained in:
parent
b85f2ea0d1
commit
767cdbb4c8
@ -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
757
tools/batools/dummymodule.py
Executable 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.')
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user