60 lines
1.6 KiB
Python
60 lines
1.6 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import ast
|
||
|
|
import re
|
||
|
|
from functools import lru_cache
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
from typing_extensions import TypeAlias
|
||
|
|
|
||
|
|
ActionParseResult: TypeAlias = "tuple[str, str, tuple[object, ...]]"
|
||
|
|
"""An action is its name and the arbitrary tuple of its arguments."""
|
||
|
|
|
||
|
|
|
||
|
|
class SkipAction(Exception):
|
||
|
|
"""Raise in an action to skip the action (and allow any parent bindings to run)."""
|
||
|
|
|
||
|
|
|
||
|
|
class ActionError(Exception):
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
re_action_args = re.compile(r"([\w\.]+)\((.*)\)")
|
||
|
|
|
||
|
|
|
||
|
|
@lru_cache(maxsize=1024)
|
||
|
|
def parse(action: str) -> ActionParseResult:
|
||
|
|
"""Parses an action string.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
action: String containing action.
|
||
|
|
|
||
|
|
Raises:
|
||
|
|
ActionError: If the action has invalid syntax.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Action name and arguments.
|
||
|
|
"""
|
||
|
|
args_match = re_action_args.match(action)
|
||
|
|
if args_match is not None:
|
||
|
|
action_name, action_args_str = args_match.groups()
|
||
|
|
if action_args_str:
|
||
|
|
try:
|
||
|
|
# We wrap `action_args_str` to be able to disambiguate the cases where
|
||
|
|
# the list of arguments is a comma-separated list of values from the
|
||
|
|
# case where the argument is a single tuple.
|
||
|
|
action_args: tuple[Any, ...] = ast.literal_eval(f"({action_args_str},)")
|
||
|
|
except Exception:
|
||
|
|
raise ActionError(
|
||
|
|
f"unable to parse {action_args_str!r} in action {action!r}"
|
||
|
|
)
|
||
|
|
else:
|
||
|
|
action_args = ()
|
||
|
|
else:
|
||
|
|
action_name = action
|
||
|
|
action_args = ()
|
||
|
|
|
||
|
|
namespace, _, action_name = action_name.rpartition(".")
|
||
|
|
|
||
|
|
return namespace, action_name, action_args
|