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)