Source code for arcade.utils

"""
Various utility functions.

IMPORTANT: These should be standalone and not rely on any Arcade imports
"""

import platform
import sys
from collections.abc import Callable, Generator, Iterable, MutableSequence, Sequence
from itertools import chain
from pathlib import Path
from typing import Any, Generic, TypeVar

from arcade.types import AsFloat, Point2

__all__ = [
    "as_type",
    "type_name",
    "copy_dunders_unimplemented",
    "is_iterable",
    "is_nonstr_iterable",
    "is_str_or_noniterable",
    "grow_sequence",
    "is_pyodide",
    "is_raspberry_pi",
    "get_raspberry_pi_info",
]

# Since this module forbids importing from the rest of
# Arcade, we make our own local type variables.
_T = TypeVar("_T")
_TType = TypeVar("_TType", bound=type)


[docs] class Chain(Generic[_T]): """A reusable OOP version of :py:class:`itertools.chain`. In some cases (physics engines), we need to iterate over multiple sequences of objects repeatedly. This class provides a way to do so which: * Is non-exhausting * Avoids copying all items into one joined list Arguments: components: The sequences of items to join. """ def __init__(self, *components: Sequence[_T]): self.components: list[Sequence[_T]] = list(components) def __iter__(self) -> Generator[_T, None, None]: yield from chain.from_iterable(self.components)
[docs] def as_type(item: Any) -> type: """If item is not a type, return its type. Otherwise, return item as-is. Args: item: A :py:class:`type` or instance of one. """ if isinstance(item, type): return item else: return item.__class__
[docs] def type_name(item: Any) -> str: """Get the name of item if it's a type or the name of its type if it's an instance. This is meant to help shorten debugging-related code and developer utilities. It isn't meant to be a performant tool. Args: item: A :py:class:`type` or an instance of one. """ if isinstance(item, type): return item.__name__ else: return item.__class__.__name__
[docs] def is_iterable(item: Any) -> bool: """Use :py:func:`iter` to infer whether ``item`` is iterable. .. tip:: An empty iterable still counts as an iterable. This relies on the following: #. Python's :py:func:`iter` raises a :py:class:`TypeError` if ``item`` is not iterable #. try/catch blocks are fast If you are still concerned about performance, you may want to inline the contents of this function locally since function calls may have more overhead than try/catch blocks on your Python version. Args: item: An object to pass to the built-in :py:func:`iter` function. Returns: ``True`` if the item appears to be iterable. """ try: _ = iter(item) return True except TypeError: return False
[docs] def is_nonstr_iterable(item: Any) -> bool: """``True`` if ``item`` is an iterable other than a :py:class:`str`. In addition to calling this function ``if`` and ``elif`` statements, you can also pass it as an argument to other functions. These include: * The :py:func:`.grow_sequence` utility function * Python's built-in filter function .. note:: This is the opposite of :py:func:`is_str_or_noniterable`. Args: item: Any object. Returns: Whether ``item`` is a non-string iterable. """ return not isinstance(item, str) and is_iterable(item)
[docs] def is_str_or_noniterable(item: Any) -> bool: """``True`` if ``item`` is a string or non-iterable. In addition to calling this function in ``if`` and ``elif`` statements, you can also pass it as an argument to other functions. These include: * The :py:func:`.grow_sequence` utility function * Python's built-in :py:func:`filter` function .. note:: This is the opposite of :py:func:`is_str_or_noniterable`. Args: item: Any object. Returns: ``True`` if ``item`` is a :py:class:`str` or a non-iterable object. """ return isinstance(item, str) or not is_iterable(item)
[docs] def grow_sequence( destination: MutableSequence[_T], source: _T | Iterable[_T], append_if: Callable[[_T | Iterable[_T]], bool] = is_str_or_noniterable, ) -> None: """Append when ``append_if(to_add)`` is ``True``, extend otherwise. If performance is critical, consider inlining this code. This function is meant as: * a companion to :py:data:`arcade.types.OneOrIterableOf` * an abbreviation for repetitive if-blocks in config and settings menus The default ``append_if`` value is the :py:func:`.is_str_or_noniterable` function in this module. You can pass any :py:func:`~typing.Callable` which returns: * ``True`` if we should :py:meth:`append <list.append>` * ``False`` if we should :py:meth:`extend <list.extend>` This includes both the :py:class:`callable` function and your own custom functions. For example: .. code-block:: python dest = [] def _validate_and_choose(item) -> bool: \"\"\"Raises an exception if data is invalid or a bool if not.\"\"\" ... # Okay values grow_sequence(dest, MyType(), append_when=_validate_and_choose) grow_sequence(dest, [MyType(), MyType()], append_when=_validate_and_choose) # Raises an exception grow_sequence(dest, BadType(), append_when=_validate_and_choose) Args: destination: A :py:func:`list` or other :py:class:`~typing.MutableSequence` to append to or extend. source: A value source we'll use to grow the ``destination`` sequence. append_if: A :py:func:`callable` which returns ``True`` when ``source`` should be appended. """ if append_if(source): destination.append(source) # type: ignore else: destination.extend(source) # type: ignore
[docs] def copy_dunders_unimplemented(decorated_type: _TType) -> _TType: """Decorator stubs dunders raising :py:class:`NotImplementedError`. Temp fixes https://github.com/pythonarcade/arcade/issues/2074 by stubbing the following instance methods: * :py:meth:`object.__copy__` (used by :py:func:`copy.copy`) * :py:meth:`object.__deepcopy__` (used by :py:func:`copy.deepcopy`) Example usage: .. code-block:: python import copy from arcade,utils import copy_dunders_unimplemented from arcade.hypothetical_module import HypotheticalNasty # Example usage @copy_dunders_unimplemented class CantCopy: def __init__(self, nasty_state: HypotheticalNasty): self.nasty_state = nasty_state instance = CantCopy(HypotheticalNasty()) # These raise NotImplementedError this_line_raises = copy.deepcopy(instance) this_line_also_raises = copy.copy(instance) """ def __copy__(self): # noqa raise NotImplementedError( f"{self.__class__.__name__} does not implement __copy__, but" f"you may implement it on a custom subclass." ) decorated_type.__copy__ = __copy__ # type: ignore def __deepcopy__(self, memo): # noqa raise NotImplementedError( f"{self.__class__.__name__} does not implement __deepcopy__," f" but you may implement it on a custom subclass." ) decorated_type.__deepcopy__ = __deepcopy__ # type: ignore return decorated_type
[docs] def is_pyodide() -> bool: return False
[docs] def is_raspberry_pi() -> bool: """Determine if the host is a raspberry pi.""" return get_raspberry_pi_info()[0]
[docs] def get_raspberry_pi_info() -> tuple[bool, str, str]: """ Determine if the host is a raspberry pi with additional info. Returned tuple format is:: bool (is host a raspi) str (architecture) str (model name) """ # The platform for raspi should always be linux if not sys.platform == "linux": return False, "", "" # armv7l is raspi 32 bit # aarch64 is raspi 64 bit architecture = platform.machine() model_name = "" # Check for model info file MODEL_PATH = Path("/sys/firmware/devicetree/base/model") if MODEL_PATH.exists(): try: model_name = MODEL_PATH.read_text()[:-1] if "raspberry pi" in model_name.lower(): return True, architecture, model_name except Exception: pass return False, "", ""
[docs] def unpack_asfloat_or_point(value: AsFloat | Point2) -> Point2: """ A utility method that converts a float or int into a Point2, or validates that an iterable is a Point2. .. note:: This should be inlined in hot code paths Args: value: The value to test. Returns: A Point2 that is either equal to value, or is equal to (value, value) """ if isinstance(value, float | int): x = y = value else: try: x, y = value except ValueError: raise ValueError( "value must be a float, int, or tuple-like which unpacks as two float-like values" ) except TypeError: raise TypeError( "value must be a float, int, or tuple-like unpacks as two float-like values" ) return x, y