Source code for go2.states.validation

"""
Internal infrastructure for safely cancelling long-running state execution
loops by injecting shutdown checks into user-defined code.

This system automatically modifies the AST (Abstract Syntax Tree) of state
`execute()` methods at class creation time, inserting cancellation checks
inside all `for` and `while` loops.

Purpose
-------
- Prevent infinite or blocking loops in user-written state logic
- Allow graceful shutdown when the robot or controller exits
- Avoid requiring users to manually check shutdown flags

This is an **internal system** and should not be directly used or modified
by user code.
"""

from abc import ABCMeta
import ast
import inspect
import textwrap
from typing import Any, Dict


[docs] class LoopCancellationInjector(ast.NodeTransformer): """ AST transformer that injects shutdown checks into loop bodies. This transformer modifies: - ``while`` loops - ``for`` loops by inserting a call to ``self.check_shutdown()`` as the first statement in each loop body. Notes ----- - Assumes the transformed method belongs to a class defining ``check_shutdown()`` - Used exclusively by ``CancellableMeta`` """ def create_check_node(self, lineno=0, col_offset=0) -> ast.Expr: return ast.Expr( value=ast.Call( func=ast.Attribute( value=ast.Name(id="self", ctx=ast.Load()), attr="check_shutdown", ctx=ast.Load() ), args=[], keywords=[] ), lineno=lineno, col_offset=col_offset ) def visit_While(self, node) -> Any: self.generic_visit(node) check_node = self.create_check_node( lineno=getattr(node, "lineno", 0), col_offset=getattr(node, "col_offset", 0) ) node.body.insert(0, check_node) return node def visit_For(self, node) -> Any: self.generic_visit(node) check_node = self.create_check_node( lineno=getattr(node, "lineno", 0), col_offset=getattr(node, "col_offset", 0) ) node.body.insert(0, check_node) return node
[docs] class CancellableMeta(ABCMeta): """ Metaclass that injects loop cancellation logic into `execute()` methods. If a class defines an ``execute`` method: - Its source code is retrieved - Parsed into an AST - All loops are instrumented with shutdown checks - The modified function replaces the original If any step fails, the original method is preserved. """ def __new__(mcls, name, bases, attrs): if "execute" in attrs and inspect.isfunction(attrs["execute"]): original = attrs["execute"] try: src = inspect.getsource(original) # get source code src = textwrap.dedent(src) # removing indentation -> needed for parsing with AST tree = ast.parse(src) # parse into AST injector = LoopCancellationInjector() new_tree = injector.visit(tree) ast.fix_missing_locations(new_tree) # fixes line numbers and other metadata for compilation -> cleanup env = original.__globals__.copy() local_env: Dict[str, Any] = {} compiled = compile(new_tree, filename="<ast>", mode="exec") # convert the new AST into executable python code exec(compiled, env, local_env) # runs in original context to make sure things like "self" still work # if compilation succeeds... the new function replaces the old execute new_func = local_env.get(original.__name__) if new_func is not None: new_func.__defaults__ = original.__defaults__ new_func.__kwdefaults__ = original.__kwdefaults__ attrs["execute"] = new_func else: attrs["execute"] = original except (OSError, IOError, TypeError, IndentationError, SyntaxError, ValueError) as e: attrs["execute"] = original # leave as is if exception is raised return super().__new__(mcls, name, bases, attrs)