Source code for simvx.core.resource

"""Package-resource handle.

The :class:`Resource` class is the canonical Pythonic sugar for an asset that
ships inside a Python package. It stores a ``(package, name)`` pair and
resolves lazily through :mod:`importlib.resources`.

For raw filesystem assets, just pass a path string or :class:`pathlib.Path`
directly to whichever loader you need (``AudioStream``, ``Mesh.from_obj``,
``ResourceLoader``, …). There are no URI schemes.

Examples::

    from pathlib import Path
    from simvx.core import AudioStream, Resource

    AudioStream("music/theme.ogg")                 # filesystem
    AudioStream(Path.home() / "music/theme.ogg")  # filesystem (PathLike)
    AudioStream(Resource("game.assets", "hero.wav"))  # package resource

    # The unwrapped importlib form also works:
    import importlib.resources
    AudioStream(importlib.resources.files("game.assets") / "hero.wav")
"""

from __future__ import annotations

import logging
from contextlib import contextmanager
from pathlib import Path
from typing import IO

from .asset_resolver import resolve_asset_path

log = logging.getLogger(__name__)

[docs] class Resource: """Lazy handle to a file shipped inside a Python package. Construction stores the package + filename without touching the filesystem; resolution happens on first access of :attr:`path`, :meth:`read_bytes`, or :meth:`open`. Example:: >>> r = Resource("pkgres_demo", "marker.bin") >>> r.read_bytes() b'SIMVX_PKGRES_OK\\n' """ __slots__ = ("package", "name") def __init__(self, package: str, name: str) -> None: if not package: raise ValueError("Resource package must be a non-empty string") if not name: raise ValueError("Resource name must be a non-empty string") self.package: str = package self.name: str = name
[docs] @property def path(self) -> Path: """Resolve to a filesystem :class:`Path` (strict; raises if missing).""" return resolve_asset_path(self)
[docs] def read_bytes(self) -> bytes: """Read and return the resource as bytes.""" return self.path.read_bytes()
[docs] @contextmanager def open(self, mode: str = "rb"): """Open the resource as a file. Defaults to binary read.""" handle: IO = self.path.open(mode) try: yield handle finally: handle.close()
[docs] def __repr__(self) -> str: return f"Resource({self.package!r}, {self.name!r})"
[docs] def __eq__(self, other: object) -> bool: if not isinstance(other, Resource): return NotImplemented return self.package == other.package and self.name == other.name
[docs] def __hash__(self) -> int: return hash((self.package, self.name))