Source code for pyecs.helpers.Unsafe
# ruff: noqa: B010
# pyright: reportAny=false, reportExplicitAny=false
import functools
from collections.abc import Callable
from typing import Any
from pyecs.exceptions import (
ComponentNotFoundError,
EntityNotFoundError,
OperationFailedError,
)
from pyecs.helpers.Statuses import StatusCodes
[docs]
def generate_unsafe[**P, R](
exception_type: type[Exception] = OperationFailedError, error_message: str | None = None
) -> Callable[[Callable[P, R]], Callable[P, R]]:
"""
Decorator that automatically generates an unsafe version of a method.
The unsafe version raises an exception instead of returning StatusCodes.FAILURE.
The decorated function gets a new `unsafe` attribute that is the exception-raising version.
Usage:
@generate_unsafe(ComponentNotFoundError, "Component not found on entity")
def get_component(self, entity: Entity, component_type: type[Component]) -> Component | Literal[StatusCodes.FAILURE]:
...
# This automatically generates:
# Safe version: component = world.get_component(entity, Position) # Returns FAILURE on error
# Strict version: component = world.get_component_or_raise(entity, Position) # Raises exception on error
"""
def decorator(func: Callable[P, R]) -> Callable[P, R]:
unsafe_name = f"{func.__name__}_or_raise"
@functools.wraps(func)
def unsafe_wrapper(*args: P.args, **kwargs: P.kwargs) -> Any:
result = func(*args, **kwargs)
if result == StatusCodes.FAILURE:
msg = error_message or f"{func.__name__} failed"
raise exception_type(msg)
return result
setattr(func, "_unsafe_version", (unsafe_name, unsafe_wrapper))
return func
return decorator
[docs]
def process_unsafe_methods[C](cls: type[C]) -> type[C]:
"""
Process all methods decorated with @generate_unsafe and add their unsafe versions to the class.
"""
for _, method in list(vars(cls).items()):
if hasattr(method, "_unsafe_version"):
unsafe_name, unsafe_wrapper = method._unsafe_version
setattr(cls, unsafe_name, unsafe_wrapper)
return cls
[docs]
def auto_unsafe[C](cls: type[C]) -> type[C]:
"""
Class decorator that automatically generates unsafe versions for all methods
that return StatusCodes.FAILURE.
Usage:
@auto_unsafe
class ECSWorld:
def get_component(self, ...) -> Component | Literal[StatusCodes.FAILURE]:
...
"""
for name, method in vars(cls).items():
if callable(method) and hasattr(method, "__annotations__"):
return_annotation = method.__annotations__.get("return", "")
if "StatusCodes.FAILURE" in str(return_annotation):
if "component" in name.lower():
exception = ComponentNotFoundError
message = f"Component operation '{name}' failed"
elif "entity" in name.lower():
exception = EntityNotFoundError
message = f"Entity operation '{name}' failed"
else:
exception = OperationFailedError
message = f"Operation '{name}' failed"
decorated = generate_unsafe(exception, message)(method)
setattr(cls, name, decorated)
return process_unsafe_methods(cls)