"""Module defining executable class."""
import re
from re import Pattern
from types import EllipsisType, UnionType
from typing import Any, Generator, NoReturn, Optional, Self, TypeVar
from ..datatypes import Data, String, string_to_datatype
from ..processor import PAction
from .util import EXP_VAR
_D = TypeVar("_D")
_T = TypeVar("_T", bound=tuple[Data, ...])
type ValueType = type[Data] | UnionType
[docs]
class BaseToken:
"""Base class for tokens.
:var match_re: Regex with two groups matching the token name and values
in a line.
:var value_delim_re: Regex matching the delimiter between values in the string
matched by the match_re in group two.
"""
match_re: Pattern[str]
value_delim_re: Pattern[str]
def __init__(self, lineno: int, name: str, values: tuple[Data, ...]) -> None:
"""Init function for Token class.
:param lineno: Line number.
:param name: Name of this token.
:param values: Tuple with string values of this token.
"""
self.lineno = lineno
self.name = name
self.values = self._check_value_types(values)
def _check_value_types(self, values: _T) -> _T:
"""Check if values are of the correct type.
:param values: Tuple with values to check.
:returns: The given tuple.
:raises TypeError: If value is not the correct data type.
"""
for value, value_type in zip(values, self._get_value_types()):
if isinstance(value, value_type):
continue
is_type = value.__class__.__name__
msg = f"Invalid argument of type {is_type} (expected {value_type})"
raise TypeError(f"{msg} in line {self.lineno}")
return values
def __repr__(self) -> str:
"""Return string representation."""
return f"<{self.lineno}, {self.name}, {self.values}>"
@classmethod
def _get_value_types(cls) -> Generator[ValueType, None, None]:
raise NotImplementedError
@classmethod
def _match_values(cls, value_string: str) -> list[str]:
match = re.match(EXP_VAR, value_string)
values: list[str] = []
while match is not None:
values.append(match.group(0))
value_string = value_string[len(match[0]):]
if not value_string:
return values
delim = re.match(cls.value_delim_re, value_string)
if not delim:
raise SyntaxError("Invalid syntax of values in token.")
value_string = value_string[len(delim[0]):]
match = re.match(EXP_VAR, value_string)
if not match:
raise SyntaxError("Invalid syntax of values in token.")
return values
[docs]
@classmethod
def from_string(cls, string: str, lineno: int) -> Self:
"""Create Token object from string.
:param string: String containing the token.
:param lineno: Line number of the token.
"""
token_match = cls.match_re.match(string)
if token_match is None:
raise ValueError("Token not in given string.")
name = token_match.group(1)
values = cls._match_values(token_match.group(2))
return cls(lineno, name, tuple((string_to_datatype(v, lineno) for v in values)))
[docs]
@classmethod
def matches(cls, string: str) -> bool:
"""Return bool if statement exists in the given string.
:param string: String to search in.
:returns: True if it exists.
"""
return cls.match_re.match(string) is not None
[docs]
def execute(self, context: Any) -> PAction | NoReturn:
"""Execute this token.
:param context: Context object.
:returns: PAction for processor class.
"""
raise NotImplementedError()
[docs]
def get_value(
self, index: int, default: Optional[_D] = None
) -> Optional[_D] | Data:
"""Get a value.
:param index: Index of the value in values tuple.
:param default: Default value.
:returns: The value at index or the default value if index is invalid.
"""
if 0 < index < len(self.values):
return self.values[index]
return default
[docs]
def get_string(self, index: int) -> String:
"""Get String object in values.
:param index: Index of the object in values.
:returns: The String object at the given index.
:raises TypeError: If object at given index is not of type String.
"""
value = self.values[index]
if isinstance(value, String):
return value
raise TypeError(f"Value {repr(value)} is not a string. (line {self.lineno})")
[docs]
class Token(BaseToken):
"""Token with a known number of values.
:var value_types: Data types defining the type of the user set values.
"""
value_types: tuple[ValueType, ...]
@classmethod
def _get_value_types(cls) -> Generator[ValueType, None, None]:
return (t for t in cls.value_types)
[docs]
class MultiExpToken(BaseToken):
"""Token with an unknown amount of values."""
value_types: tuple[*tuple[ValueType, ...], EllipsisType]
@classmethod
def _get_value_types(cls) -> Generator[ValueType, None, None]:
if len(cls.value_types) < 2:
raise ValueError("MultiExpToken class needs at least two value types.")
for t in cls.value_types:
if isinstance(t, EllipsisType):
break
yield t
yield cls.value_types[-2]