Source code for Karana.KUtils.MultibodyTUI.mode

from typing import get_args, Literal, Generic, TypeVar, Self

T = TypeVar("T")


[docs] class ModeCycler(Generic[T]): """Cycles through a finite list of literals representing modes It's somewhat awkward in Python to access the generic type at runtime. This class provides an abstraction that handles the gory details so that it's straightforward to bind a typing.Literal to the generic and use the literal options both in type hints and at runtime. Should be specialized with a typing.Literal - for example: FooBarOptions = typing.Literal["foo", "bar"] FooBarCycler = ModeCycler[FooBarOptions] Then FooBarCycler is a class that can be instantiated to cycle between modes "foo" and "bar". """ def __init__(self): self._index = 0 # Cannot access modes while in __init__ so we defer # to the first time it's actually needed self._cached_modes = None
[docs] def copy(self) -> Self: """Get a copy of self""" copy = self.__orig_class__() copy._index = self._index copy._cached_modes = self._cached_modes return copy
[docs] def __len__(self) -> int: """The number of modes""" return len(self._modes_nocopy)
@property def mode(self) -> T: """The current mode""" return self._modes_nocopy[self._index] @mode.setter def mode(self, new_mode: T, /): for i, mode in enumerate(self._modes_nocopy): if new_mode == mode: self._index = i return raise ValueError(f"{new_mode} is not a valid mode") @property def modes(self) -> list[T]: """The list of available modes""" # Return a copy for encapsulation return self._modes_nocopy[:] @property def _modes_nocopy(self) -> list[T]: # An accessor for the modes list that avoids creating a copy if not self._cached_modes: self._cached_modes = self._compute_mode_list() return self._cached_modes @property def index(self) -> int: """The current index into the mode list""" return self._index @index.setter def index(self, index: int, /): """Set the mode by index ensuring it's in bounds""" assert index >= 0 and index < len(self) self._index = index
[docs] def next(self): """Change to the next mode in the list""" self._index = (self._index + 1) % len(self)
[docs] def prev(self): """Change to the previous mode in the list""" self._index = (self._index - 1) % len(self)
[docs] def reset(self): """Reset to the default mode""" self._index = 0
def _compute_mode_list(self) -> list[T]: """Get Literal options from the generic type""" # Note: cannot be called in __init__ orig_class = getattr(self, "__orig_class__", None) if not orig_class: raise RuntimeError("__orig_class__ not defined") type_params = get_args(orig_class) if len(type_params) != 1: raise TypeError(f"concrete type should be a single typing.Literal, not {type_params}") if not hasattr(type_params[0], "__origin__"): raise TypeError(f"concrete type should be a single typing.Literal, not {type_params}") if type_params[0].__origin__ != Literal: raise TypeError(f"concrete type should be a single typing.Literal, not {type_params}") modes = get_args(type_params[0]) return list(modes)