Skip to content

xnano.utils.introspection

xnano.utils.introspection

xnano.utils.introspection


Hook signature inspection and safe state-expression evaluation.

State/field expressions (@on_state("count > 0")) are evaluated by walking a parsed AST with a small, fixed set of operators rather than calling eval: names resolve only against the supplied namespace, calls are limited to a whitelist of pure builtins, and attribute access to dunder names is refused. There is no code execution path, so a crafted expression can read declared state but cannot reach the interpreter.

Functions:

get_compiled_state_expression

get_compiled_state_expression(
    expression: str,
) -> Expression | None

Return the cached parsed AST for expression.

Parses and caches on first use; a source that fails to parse is cached as None so it is not retried on every call.

Parameters:

  • expression (str) –

    The expression source to parse.

Returns:

  • Expression | None

    The parsed eval-mode AST, or None when expression does not parse.

Source code in xnano/utils/introspection.py
def get_compiled_state_expression(expression: str) -> ast.Expression | None:
    """Return the cached parsed AST for ``expression``.

    Parses and caches on first use; a source that fails to parse is cached
    as ``None`` so it is not retried on every call.

    Args:
        expression: The expression source to parse.

    Returns:
        The parsed ``eval``-mode AST, or ``None`` when ``expression`` does
            not parse.
    """
    if expression in _PARSED_EXPRESSION_CACHE:
        return _PARSED_EXPRESSION_CACHE[expression]
    try:
        tree: ast.Expression | None = ast.parse(expression, mode="eval")
    except SyntaxError:
        tree = None
    _PARSED_EXPRESSION_CACHE[expression] = tree
    return tree

evaluate_state_expression

evaluate_state_expression(
    expression: str, state: StateT
) -> bool

Evaluate expression against state's attributes.

Safe by construction — the expression is walked node by node over a fixed operator/builtin set, never executed. Returns False on any error, including an unknown name, an unsupported construct, or an AttributeError. The parse is cached by source, since hooks re-evaluate the same expression on every tick/frame.

Parameters:

  • expression (str) –

    The expression to evaluate.

  • state (StateT) –

    The object whose attributes the expression reads.

Returns:

  • bool

    The truthiness of the evaluated expression.

Source code in xnano/utils/introspection.py
def evaluate_state_expression(
    expression: str,
    state: StateT,
) -> bool:
    """Evaluate ``expression`` against ``state``'s attributes.

    Safe by construction — the expression is walked node by node over a
    fixed operator/builtin set, never executed. Returns ``False`` on any
    error, including an unknown name, an unsupported construct, or an
    ``AttributeError``. The parse is cached by source, since hooks
    re-evaluate the same expression on every tick/frame.

    Args:
        expression: The expression to evaluate.
        state: The object whose attributes the expression reads.

    Returns:
        The truthiness of the evaluated expression.
    """
    if state is None:
        return False
    tree = get_compiled_state_expression(expression)
    if tree is None:
        return False
    try:
        return bool(_evaluate_node(tree.body, _build_expression_names(state)))
    except Exception:
        return False

is_reference_expression

is_reference_expression(expression: str) -> bool

Return whether expression is a bare reference (see above).

Bare references drive mutation-triggered hooks: the watcher fires when the referenced value changes rather than each frame it is truthy.

Source code in xnano/utils/introspection.py
def is_reference_expression(expression: str) -> bool:
    """Return whether ``expression`` is a bare reference (see above).

    Bare references drive mutation-triggered hooks: the watcher fires when
    the referenced value changes rather than each frame it is truthy.
    """
    tree = get_compiled_state_expression(expression)
    return tree is not None and _is_reference_node(tree.body)

evaluate_reference_value

evaluate_reference_value(
    expression: str, state: Any
) -> Any

Return the current value of a bare reference, or _MISSING.

Used by mutation-triggered @on_state/@on_field hooks to read the watched value each frame. Any evaluation error (unknown name, missing attribute) resolves to _MISSING so the watcher simply holds its previous value instead of firing.

Source code in xnano/utils/introspection.py
def evaluate_reference_value(expression: str, state: Any) -> Any:
    """Return the current value of a bare reference, or ``_MISSING``.

    Used by mutation-triggered ``@on_state``/``@on_field`` hooks to read
    the watched value each frame. Any evaluation error (unknown name,
    missing attribute) resolves to ``_MISSING`` so the watcher simply
    holds its previous value instead of firing.
    """
    tree = get_compiled_state_expression(expression)
    if tree is None:
        return _MISSING
    try:
        return _evaluate_node(tree.body, _build_expression_names(state))
    except Exception:
        return _MISSING

get_first_function_parameter_type

get_first_function_parameter_type(
    function: Callable,
) -> type | None

Returns the type annotation of the first parameter of a function.

Parameters:

  • function (Callable) –

    The function to get the first parameter type from.

Returns:

  • type | None

    The type annotation of the first parameter of the function (if one exists).

Source code in xnano/utils/introspection.py
def get_first_function_parameter_type(function: Callable) -> type | None:
    """Returns the type annotation of the first parameter of a
    function.

    Args:
        function: The function to get the first parameter type from.

    Returns:
        The type annotation of the first parameter of the function (if
            one exists).
    """
    cached = _FIRST_PARAM_TYPE_CACHE.get(function)
    if cached is not None or function in _FIRST_PARAM_TYPE_CACHE:
        return cached

    try:
        signature = inspect.signature(function)
    except (TypeError, ValueError):
        _FIRST_PARAM_TYPE_CACHE[function] = None
        return None

    parameters = list(signature.parameters.values())
    if not parameters:
        _FIRST_PARAM_TYPE_CACHE[function] = None
        return None

    first_parameter = parameters[0]
    if first_parameter.name in ("self", "cls"):
        if len(parameters) < 2:
            _FIRST_PARAM_TYPE_CACHE[function] = None
            return None

        annotation = parameters[1].annotation
    else:
        annotation = first_parameter.annotation

    if annotation is inspect.Parameter.empty:
        _FIRST_PARAM_TYPE_CACHE[function] = None
        return None

    if isinstance(annotation, type):
        _FIRST_PARAM_TYPE_CACHE[function] = annotation
        return annotation

    _FIRST_PARAM_TYPE_CACHE[function] = None
    return None

get_function_extra_parameter_count

get_function_extra_parameter_count(
    function: Callable,
) -> int

Return parameters after self / cls on a callable.

Parameters:

  • function (Callable) –

    The callable to inspect.

Returns:

  • int

    The number of parameters following self or cls.

Source code in xnano/utils/introspection.py
def get_function_extra_parameter_count(function: Callable) -> int:
    """Return parameters after ``self`` / ``cls`` on a callable.

    Args:
        function: The callable to inspect.

    Returns:
        The number of parameters following ``self`` or ``cls``.
    """
    if function in _EXTRA_PARAM_COUNT_CACHE:
        return _EXTRA_PARAM_COUNT_CACHE[function]

    try:
        signature = inspect.signature(function)
    except (TypeError, ValueError):
        _EXTRA_PARAM_COUNT_CACHE[function] = 0
        return 0

    parameters = list(signature.parameters.values())
    if not parameters:
        _EXTRA_PARAM_COUNT_CACHE[function] = 0
        return 0

    start = 1 if parameters[0].name in ("self", "cls") else 0
    count = len(parameters) - start
    _EXTRA_PARAM_COUNT_CACHE[function] = count
    return count