# Released under the MIT License. See LICENSE for details.
#
"""Codegen for type-safe asset-reference wrapper modules.
Given a package's ``apverid`` and its asset listing (logical path + single-
char kind code per asset), emit a Python module exposing per-kind roots
(``textures``, ``meshes``, ...) whose members are type-safe, ergonomic
references. A leaf reads as a property yielding the kind's reference type
(``zoe_icon: TextureRef``); a subdir nests another group class. Precise
types live in an ``if TYPE_CHECKING`` shadow; runtime resolution is the
dynamic :class:`AssetRefDir` (no per-asset runtime class).
This mirrors the structure of both the client-side asset-package wrappers
(``bauiv1.builtinassets``: ``TexturesGroup`` with asset properties) and the
language-string wrappers (``bacommon.langstr`` codegen) -- same ``FooGroup``
hierarchy, assets exposed as properties. Logical paths split on ``/`` into
subdir nesting (``textures/ui/star`` -> ``textures.ui.star``); each segment
must be an identifier.
"""
from __future__ import annotations # Docs-generation hack.
#: Reference type name emitted for each single-char asset kind code.
_KIND_TYPES = {'t': 'TextureRef', 'm': 'MeshRef'}
#: A nested name tree: a leaf maps a name to its kind code, a subdir maps a
#: name to a nested tree (paths split on ``/``).
type _Tree = dict[str, 'str | _Tree']
def _build_tree(assets: list[tuple[str, str]]) -> _Tree:
"""Split each ``foo/bar`` logical path into a nested name tree."""
root: _Tree = {}
for path, kind in assets:
parts = path.split('/')
node = root
for part in parts[:-1]:
child = node.get(part)
if child is None:
child = {}
node[part] = child
if not isinstance(child, dict):
raise ValueError(
f'path {path!r} collides with an asset at {part!r}'
)
node = child
leaf = parts[-1]
if leaf in node:
raise ValueError(f'duplicate asset path {path!r}')
node[leaf] = kind
return root
def _pascal_case(segment: str) -> str:
"""``some_thing`` -> ``SomeThing``."""
return ''.join(part.capitalize() for part in segment.split('_'))
def _dir_class_name(segments: list[str]) -> str:
"""Public group class name (``['textures'] -> TexturesGroup``)."""
return ''.join(_pascal_case(s) for s in segments) + 'Group'
def _emit_dir_classes(out: list[str], tree: _Tree, segments: list[str]) -> None:
"""Emit the typed group class for this dir (children first).
A subdir is a typed attribute pointing at its group class; a leaf asset
is a property typed by its kind's reference type.
"""
for name in sorted(tree):
value = tree[name]
if isinstance(value, dict):
_emit_dir_classes(out, value, [*segments, name])
label = '/'.join(segments)
out += [
f' class {_dir_class_name(segments)}:',
f' """Type-safe asset references ({label})."""',
'',
]
for name in sorted(tree):
value = tree[name]
if isinstance(value, dict):
sub = _dir_class_name([*segments, name])
out.append(f' {name}: {sub}')
else:
out.append(f' {name}: {_KIND_TYPES[value]}')
out.append('')
[docs]
def generate_asset_ref_wrapper_module(
apverid: str, assets: list[tuple[str, str]]
) -> str:
"""Return the ``.py`` source for a package's type-safe ref wrapper.
``assets`` is the package's asset listing as ``(logical_path, kind_code)``
pairs (e.g. ``('textures/zoe_icon', 't')``). The output is valid but not
auto-formatted (the ``_TREE`` literal in particular); a caller writing it
to the tree should run the formatter before committing.
"""
for path, kind in assets:
if kind not in _KIND_TYPES:
raise ValueError(f'unknown asset kind {kind!r} for {path!r}')
for part in path.split('/'):
if not part.isidentifier():
raise ValueError(f'bad path segment {part!r} in {path!r}')
tree = _build_tree(sorted(assets))
# The kind types referenced anywhere in the tree (for the import line).
used_types = sorted({_KIND_TYPES[k] for _p, k in assets})
join_used_types = ', '.join(used_types)
out: list[str] = [
'# Released under the MIT License. See LICENSE for details.',
'#',
'# AUTO-GENERATED by bacommon.assetref codegen; do not edit by hand.',
f'"""Type-safe asset references for {apverid}."""',
'',
'from typing import TYPE_CHECKING',
'',
'from bacommon.assetref import AssetRefDir',
'',
f"__asset_package__ = '{apverid}'",
'',
'if TYPE_CHECKING:',
f' from bacommon.assetref import {join_used_types}',
'',
]
# One group-class hierarchy + module-level accessor per top-level kind
# dir (``textures``, ``meshes``, ...), mirroring the client wrappers.
for top in sorted(tree):
subtree = tree[top]
assert isinstance(subtree, dict)
_emit_dir_classes(out, subtree, [top])
out.append(f' {top}: {_dir_class_name([top])}')
out += [
'',
f'_TREE = {tree!r}',
'',
'if not TYPE_CHECKING:',
]
for top in sorted(tree):
args = f'__asset_package__, _TREE[{top!r}], {top!r}'
out.append(f' {top} = AssetRefDir({args})')
out.append('')
return '\n'.join(out)
# 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)
import asyncio
from concurrent.futures import Future
from pathlib import Path
from enum import Enum