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))