Module scrolls.interpreter.callhandler

Call handler protocols and implementations.

Expand source code
"""
Call handler protocols and implementations.
"""

import abc
import dataclasses
import logging
import typing as t
import uuid

from .. import ast
from . import interpreter_errors, state

__all__ = (
    "CallHandler",
    "ScrollCallback",
    "Initializer",
    "RuntimeCall",
    "RuntimeCallHandler",
    "CallbackCallHandler",
    "CallbackControlHandler",
    "CallbackCommandHandler",
    "CallbackExpansionHandler",
    "CallHandlerContainer",
    "MutableCallHandlerContainer",
    "BaseCallHandlerContainer",
    "ChoiceCallHandlerContainer",
)


logger = logging.getLogger(__name__)
T = t.TypeVar("T")
T_co = t.TypeVar("T_co", covariant=True)
AnyContextTV = t.TypeVar("AnyContextTV", bound='state.InterpreterContext')


class CallHandler(t.Protocol[T_co]):
    """
    The minimum interface required to implement a call handler.
    """

    def handle_call(self, context: AnyContextTV) -> T_co:
        """
        Handle a call. An `scrolls.interpreter.state.InterpreterContext` object will be passed in reflecting the state of the `scrolls.interpreter.run.Interpreter` for
        this call.
        """
        ...

    def __contains__(self, command_name: str) -> bool: ...


class ScrollCallback(t.Protocol[T_co]):
    """
    Protocol for callbacks passed into `scrolls.interpreter.callhandler.CallbackCallHandler` objects.

    A `ScrollCallback` is any `typing.Callable` that takes an `scrolls.interpreter.state.InterpreterContext` or subclass as its only parameter.
    """

    def __call__(self, context: AnyContextTV) -> T_co: ...


class Initializer(abc.ABC):
    """
    The base class for initializers. Initializers are used by the interpreter to set up `scrolls.interpreter.state.InterpreterContext` instances
    immediately before a script is run. Initializers are considered to implement the `CallHandler` interface, even though
    they don't actually handle calls.
    """

    @abc.abstractmethod
    def handle_call(self, context: AnyContextTV) -> None:
        """
        Initialize an `scrolls.interpreter.state.InterpreterContext` or subclass.
        """
        ...

    def __contains__(self, command_name: str) -> bool:
        return False


@dataclasses.dataclass
class RuntimeCall:
    """
    A simple runtime call that is implemented by some Scrolls code.

    .. WARNING::
        Instances of this class are created automatically by `scrolls.interpreter.callhandler.RuntimeCallHandler`.
    """

    name: str
    """The name of the call."""

    node: ast.ASTNode
    """The statement node that should be run when this call is executed."""

    params: t.Sequence[str]
    """The names of the parameters, corresponding to the names of the local variables created when this call
    is executed.
    """

    collect_param: t.Optional[str]
    """The name of the collect parameter, if any. This will always be the last parameter, and will
    collect all extra arguments fed into this call and interpret them as a string vector. In other words, this is
    the `*args` parameter, for Scrolls.
    """


class RuntimeCallHandler(t.Generic[T_co]):
    """
    A basic call handler that maps names to AST nodes.
    """

    def __init__(self) -> None:
        self.calls: t.MutableMapping[str, RuntimeCall] = {}

    def define(self, name: str, node: ast.ASTNode, params: t.Sequence[str]) -> None:
        """
        Defines a new call implemented with Scrolls code. See `RuntimeCall`.
        """
        collect_param: t.Optional[str] = None

        if params and params[-1].startswith("*"):
            collect_param = params[-1][1:]
            params = params[:-1]

        call = RuntimeCall(
            name,
            node,
            params,
            collect_param
        )

        self.calls[name] = call

    def undefine(self, name: str) -> None:
        """
        Delete a defined runtime call.
        """
        del self.calls[name]

    def handle_call(self, context: state.InterpreterContext) -> T_co:
        call = self.calls[context.call_name]

        # Arg length check
        if call.collect_param is None:
            if len(call.params) != len(context.args):
                raise interpreter_errors.InterpreterError(
                    context,
                    f"{context.call_name}: Invalid # of arguments (expected {len(call.params)})"
                )
        else:
            if len(context.args) < len(call.params) - 1:
                raise interpreter_errors.InterpreterError(
                    context,
                    f"{context.call_name}: Invalid # of arguments (expected at least {len(call.params)})"
                )

        params = list(call.params)

        if call.collect_param is None:
            args = context.args
        else:
            params.append(call.collect_param)
            collected = context.args[len(call.params):]
            args = list(context.args[:len(call.params)])
            args.append(" ".join(collected))

        # New scope must be created. We're running Scrolls code to implement this call, so it might trample
        # what's been defined otherwise. Plus, we don't want our call arguments to continue existing
        # after we're done.
        context.vars.new_scope()
        for param, arg in zip(params, args):
            context.set_var(param, arg)

        context.call_context.runtime_call = True
        try:
            # Interpret the body of the call.
            context.interpreter.interpret_statement(context, call.node)
        except interpreter_errors.InterpreterReturn:
            pass

        context.vars.destroy_scope()

        # TODO Fix typing here
        return t.cast(T_co, context.call_context.return_value)

    def __contains__(self, command_name: str) -> bool:
        return command_name in self.calls


class CallbackCallHandler(t.Generic[T_co]):
    """
    A basic call handler that uses `typing.Callable` (`ScrollCallback`) to
    implement a call handler.
    """

    def __init__(self) -> None:
        self.calls: t.MutableMapping[str, ScrollCallback[T_co]] = {}
        self.aliases: t.MutableMapping[str, str] = {}

    def add_call(self, name: str, command: ScrollCallback[T_co]) -> None:
        """
        Add a call.
        """
        self.calls[name] = command

    def add_alias(self, alias: str, name: str) -> None:
        """Adds an alias for the named call. The call can then be executed by either it's real name or any of the
        defined aliases."""
        self.aliases[alias] = name

    def remove_call(self, name: str) -> None:
        """Remove a call. Note that this also removes all of its associated aliases."""
        del self.calls[name]

        # Delete all aliases associated with the name.
        for key, value in self.aliases.items():
            if value == name:
                del self.aliases[key]

    def get_callback(self, name: str) -> ScrollCallback[T_co]:
        """Get the callback for a call."""
        if name in self.calls:
            return self.calls[name]

        return self.calls[self.aliases[name]]

    def handle_call(self, context: state.InterpreterContext) -> T_co:
        return self.get_callback(context.call_name)(context)

    def __contains__(self, command_name: str) -> bool:
        logger.debug(f"{self.__class__.__qualname__}: __contains__({command_name})")
        return (
                command_name in self.calls or
                command_name in self.aliases
        )


CallbackCommandHandler = CallbackCallHandler[None]
"""A basic command handler, shortcut for `CallbackCallHandler[None]`."""

CallbackControlHandler = CallbackCallHandler[None]
"""A basic control handler, shortcut for `CallbackCallHandler[None]`."""

CallbackExpansionHandler = CallbackCallHandler[str]
"""A basic expansion handler, shortcut for `CallbackCallHandler[str]`."""


class CallHandlerContainer(t.Protocol[T_co]):
    """
    A read-only `CallHandler` container.
    """

    def get(self, name: str) -> CallHandler[T_co]: ...

    """Gets a call handler by name."""

    def get_for_call(self, name: str) -> CallHandler[T_co]: ...

    """Gets a call handler for the named call."""

    def __iter__(self) -> t.Iterator[CallHandler[T_co]]: ...


class MutableCallHandlerContainer(CallHandlerContainer[T], t.Protocol[T]):
    """
    A mutable `CallHandler` container.
    """

    def add(self, handler: CallHandler[T], name: str = "") -> None: ...

    """Add a call handler to this container.

    If `name` is not specified, then a unique name should be generated. The specific name generated is up to the
    implementor.
    """

    def remove(self, handler: t.Union[CallHandler[T], str]) -> None: ...

    """Remove a call handler from this container."""


class BaseCallHandlerContainer(t.Generic[T]):
    """
    Generic container for `CallHandler` implementors.
    """

    def __init__(self) -> None:
        self._handlers: t.MutableMapping[str, CallHandler[T]] = {}

    def add(self, handler: CallHandler[T], name: str = "") -> None:
        """Add a call handler to this container.

        If `name` is not specified, then a unique name will be generated through `uuid.uuid4`.
        """
        if not name:
            name = str(uuid.uuid4())

        logger.debug(f"Register call handler type {handler.__class__.__qualname__} name {name}")
        self._handlers[name] = handler

    def add_all(self, handlers: t.Sequence[CallHandler[T]]) -> None:
        """Shortcut, adds all handlers in a list at once."""
        for handler in handlers:
            self.add(handler)

    def find(self, handler: t.Union[CallHandler[T], str]) -> tuple[str, CallHandler[T]]:
        """Find a call handler.

        Args:
            handler: The handler to search for. It may be a CallHandler object, or the name of the handler to search for.

        Returns:
            A `tuple` of the form `(name, call_handler)`.
        """
        if isinstance(handler, str):
            return handler, self._handlers[handler]
        else:
            for k, v in self._handlers.items():
                if v is handler:
                    return k, v

            raise KeyError(repr(handler))

    def remove(self, handler: t.Union[CallHandler[T], str]) -> None:
        """Remove a call handler from this container."""
        k, v = self.find(handler)
        del self._handlers[k]

    def get(self, name: str) -> CallHandler[T]:
        """Gets a call handler by name."""
        return self._handlers[name]

    def get_for_call(self, name: str) -> CallHandler[T]:
        """
        Get the handler for a given command name.
        """
        logger.debug(f"get_for_call: {name}")
        for handler in self._handlers.values():
            if name in handler:
                return handler

        raise KeyError(name)

    def __iter__(self) -> t.Iterator[CallHandler[T]]:
        yield from self._handlers.values()


class ChoiceCallHandlerContainer(t.Generic[T]):
    """
    A call handler tries to handle a call with a sequence of call handler containers, one after another.
    """

    def __init__(self, *containers: CallHandlerContainer[T]):
        self.containers = containers

    def get(self, name: str) -> CallHandler[T]:
        for container in self.containers:
            try:
                return container.get(name)
            except KeyError:
                pass

        raise KeyError(name)

    def get_for_call(self, name: str) -> CallHandler[T]:
        logger.debug(f"ChoiceCallHandlerContainer: get_for_call {name}")
        for container in self.containers:
            try:
                return container.get_for_call(name)
            except KeyError:
                logger.debug(f"fail on {container.__class__.__qualname__}")
                pass

        raise KeyError(name)

    def __iter__(self) -> t.Iterator[CallHandler[T]]:
        for container in self.containers:
            yield from container

Global variables

var CallbackCommandHandler

A basic command handler, shortcut for CallbackCallHandler[None].

var CallbackControlHandler

A basic control handler, shortcut for CallbackCallHandler[None].

var CallbackExpansionHandler

A basic expansion handler, shortcut for CallbackCallHandler[str].

Classes

class BaseCallHandlerContainer

Generic container for CallHandler implementors.

Expand source code
class BaseCallHandlerContainer(t.Generic[T]):
    """
    Generic container for `CallHandler` implementors.
    """

    def __init__(self) -> None:
        self._handlers: t.MutableMapping[str, CallHandler[T]] = {}

    def add(self, handler: CallHandler[T], name: str = "") -> None:
        """Add a call handler to this container.

        If `name` is not specified, then a unique name will be generated through `uuid.uuid4`.
        """
        if not name:
            name = str(uuid.uuid4())

        logger.debug(f"Register call handler type {handler.__class__.__qualname__} name {name}")
        self._handlers[name] = handler

    def add_all(self, handlers: t.Sequence[CallHandler[T]]) -> None:
        """Shortcut, adds all handlers in a list at once."""
        for handler in handlers:
            self.add(handler)

    def find(self, handler: t.Union[CallHandler[T], str]) -> tuple[str, CallHandler[T]]:
        """Find a call handler.

        Args:
            handler: The handler to search for. It may be a CallHandler object, or the name of the handler to search for.

        Returns:
            A `tuple` of the form `(name, call_handler)`.
        """
        if isinstance(handler, str):
            return handler, self._handlers[handler]
        else:
            for k, v in self._handlers.items():
                if v is handler:
                    return k, v

            raise KeyError(repr(handler))

    def remove(self, handler: t.Union[CallHandler[T], str]) -> None:
        """Remove a call handler from this container."""
        k, v = self.find(handler)
        del self._handlers[k]

    def get(self, name: str) -> CallHandler[T]:
        """Gets a call handler by name."""
        return self._handlers[name]

    def get_for_call(self, name: str) -> CallHandler[T]:
        """
        Get the handler for a given command name.
        """
        logger.debug(f"get_for_call: {name}")
        for handler in self._handlers.values():
            if name in handler:
                return handler

        raise KeyError(name)

    def __iter__(self) -> t.Iterator[CallHandler[T]]:
        yield from self._handlers.values()

Ancestors

  • typing.Generic

Methods

def add(self, handler: CallHandler[~T], name: str = '') ‑> None

Add a call handler to this container.

If name is not specified, then a unique name will be generated through uuid.uuid4.

Expand source code
def add(self, handler: CallHandler[T], name: str = "") -> None:
    """Add a call handler to this container.

    If `name` is not specified, then a unique name will be generated through `uuid.uuid4`.
    """
    if not name:
        name = str(uuid.uuid4())

    logger.debug(f"Register call handler type {handler.__class__.__qualname__} name {name}")
    self._handlers[name] = handler
def add_all(self, handlers: Sequence[CallHandler[~T]]) ‑> None

Shortcut, adds all handlers in a list at once.

Expand source code
def add_all(self, handlers: t.Sequence[CallHandler[T]]) -> None:
    """Shortcut, adds all handlers in a list at once."""
    for handler in handlers:
        self.add(handler)
def find(self, handler: Union[CallHandler[~T], str]) ‑> tuple

Find a call handler.

Args

handler
The handler to search for. It may be a CallHandler object, or the name of the handler to search for.

Returns

A tuple of the form (name, call_handler).

Expand source code
def find(self, handler: t.Union[CallHandler[T], str]) -> tuple[str, CallHandler[T]]:
    """Find a call handler.

    Args:
        handler: The handler to search for. It may be a CallHandler object, or the name of the handler to search for.

    Returns:
        A `tuple` of the form `(name, call_handler)`.
    """
    if isinstance(handler, str):
        return handler, self._handlers[handler]
    else:
        for k, v in self._handlers.items():
            if v is handler:
                return k, v

        raise KeyError(repr(handler))
def get(self, name: str) ‑> CallHandler[~T]

Gets a call handler by name.

Expand source code
def get(self, name: str) -> CallHandler[T]:
    """Gets a call handler by name."""
    return self._handlers[name]
def get_for_call(self, name: str) ‑> CallHandler[~T]

Get the handler for a given command name.

Expand source code
def get_for_call(self, name: str) -> CallHandler[T]:
    """
    Get the handler for a given command name.
    """
    logger.debug(f"get_for_call: {name}")
    for handler in self._handlers.values():
        if name in handler:
            return handler

    raise KeyError(name)
def remove(self, handler: Union[CallHandler[~T], str]) ‑> None

Remove a call handler from this container.

Expand source code
def remove(self, handler: t.Union[CallHandler[T], str]) -> None:
    """Remove a call handler from this container."""
    k, v = self.find(handler)
    del self._handlers[k]
class CallHandler (*args, **kwargs)

The minimum interface required to implement a call handler.

Expand source code
class CallHandler(t.Protocol[T_co]):
    """
    The minimum interface required to implement a call handler.
    """

    def handle_call(self, context: AnyContextTV) -> T_co:
        """
        Handle a call. An `scrolls.interpreter.state.InterpreterContext` object will be passed in reflecting the state of the `scrolls.interpreter.run.Interpreter` for
        this call.
        """
        ...

    def __contains__(self, command_name: str) -> bool: ...

Ancestors

  • typing.Protocol
  • typing.Generic

Methods

def handle_call(self, context: ~AnyContextTV) ‑> +T_co

Handle a call. An InterpreterContext object will be passed in reflecting the state of the Interpreter for this call.

Expand source code
def handle_call(self, context: AnyContextTV) -> T_co:
    """
    Handle a call. An `scrolls.interpreter.state.InterpreterContext` object will be passed in reflecting the state of the `scrolls.interpreter.run.Interpreter` for
    this call.
    """
    ...
class CallHandlerContainer (*args, **kwargs)

A read-only CallHandler container.

Expand source code
class CallHandlerContainer(t.Protocol[T_co]):
    """
    A read-only `CallHandler` container.
    """

    def get(self, name: str) -> CallHandler[T_co]: ...

    """Gets a call handler by name."""

    def get_for_call(self, name: str) -> CallHandler[T_co]: ...

    """Gets a call handler for the named call."""

    def __iter__(self) -> t.Iterator[CallHandler[T_co]]: ...

Ancestors

  • typing.Protocol
  • typing.Generic

Subclasses

Methods

def get(self, name: str) ‑> CallHandler[+T_co]
Expand source code
def get(self, name: str) -> CallHandler[T_co]: ...
def get_for_call(self, name: str) ‑> CallHandler[+T_co]
Expand source code
def get_for_call(self, name: str) -> CallHandler[T_co]: ...
class CallbackCallHandler

A basic call handler that uses typing.Callable (ScrollCallback) to implement a call handler.

Expand source code
class CallbackCallHandler(t.Generic[T_co]):
    """
    A basic call handler that uses `typing.Callable` (`ScrollCallback`) to
    implement a call handler.
    """

    def __init__(self) -> None:
        self.calls: t.MutableMapping[str, ScrollCallback[T_co]] = {}
        self.aliases: t.MutableMapping[str, str] = {}

    def add_call(self, name: str, command: ScrollCallback[T_co]) -> None:
        """
        Add a call.
        """
        self.calls[name] = command

    def add_alias(self, alias: str, name: str) -> None:
        """Adds an alias for the named call. The call can then be executed by either it's real name or any of the
        defined aliases."""
        self.aliases[alias] = name

    def remove_call(self, name: str) -> None:
        """Remove a call. Note that this also removes all of its associated aliases."""
        del self.calls[name]

        # Delete all aliases associated with the name.
        for key, value in self.aliases.items():
            if value == name:
                del self.aliases[key]

    def get_callback(self, name: str) -> ScrollCallback[T_co]:
        """Get the callback for a call."""
        if name in self.calls:
            return self.calls[name]

        return self.calls[self.aliases[name]]

    def handle_call(self, context: state.InterpreterContext) -> T_co:
        return self.get_callback(context.call_name)(context)

    def __contains__(self, command_name: str) -> bool:
        logger.debug(f"{self.__class__.__qualname__}: __contains__({command_name})")
        return (
                command_name in self.calls or
                command_name in self.aliases
        )

Ancestors

  • typing.Generic

Subclasses

Methods

def add_alias(self, alias: str, name: str) ‑> None

Adds an alias for the named call. The call can then be executed by either it's real name or any of the defined aliases.

Expand source code
def add_alias(self, alias: str, name: str) -> None:
    """Adds an alias for the named call. The call can then be executed by either it's real name or any of the
    defined aliases."""
    self.aliases[alias] = name
def add_call(self, name: str, command: ScrollCallback[+T_co]) ‑> None

Add a call.

Expand source code
def add_call(self, name: str, command: ScrollCallback[T_co]) -> None:
    """
    Add a call.
    """
    self.calls[name] = command
def get_callback(self, name: str) ‑> ScrollCallback[+T_co]

Get the callback for a call.

Expand source code
def get_callback(self, name: str) -> ScrollCallback[T_co]:
    """Get the callback for a call."""
    if name in self.calls:
        return self.calls[name]

    return self.calls[self.aliases[name]]
def handle_call(self, context: InterpreterContext) ‑> +T_co
Expand source code
def handle_call(self, context: state.InterpreterContext) -> T_co:
    return self.get_callback(context.call_name)(context)
def remove_call(self, name: str) ‑> None

Remove a call. Note that this also removes all of its associated aliases.

Expand source code
def remove_call(self, name: str) -> None:
    """Remove a call. Note that this also removes all of its associated aliases."""
    del self.calls[name]

    # Delete all aliases associated with the name.
    for key, value in self.aliases.items():
        if value == name:
            del self.aliases[key]
class ChoiceCallHandlerContainer (*containers: CallHandlerContainer[~T])

A call handler tries to handle a call with a sequence of call handler containers, one after another.

Expand source code
class ChoiceCallHandlerContainer(t.Generic[T]):
    """
    A call handler tries to handle a call with a sequence of call handler containers, one after another.
    """

    def __init__(self, *containers: CallHandlerContainer[T]):
        self.containers = containers

    def get(self, name: str) -> CallHandler[T]:
        for container in self.containers:
            try:
                return container.get(name)
            except KeyError:
                pass

        raise KeyError(name)

    def get_for_call(self, name: str) -> CallHandler[T]:
        logger.debug(f"ChoiceCallHandlerContainer: get_for_call {name}")
        for container in self.containers:
            try:
                return container.get_for_call(name)
            except KeyError:
                logger.debug(f"fail on {container.__class__.__qualname__}")
                pass

        raise KeyError(name)

    def __iter__(self) -> t.Iterator[CallHandler[T]]:
        for container in self.containers:
            yield from container

Ancestors

  • typing.Generic

Methods

def get(self, name: str) ‑> CallHandler[~T]
Expand source code
def get(self, name: str) -> CallHandler[T]:
    for container in self.containers:
        try:
            return container.get(name)
        except KeyError:
            pass

    raise KeyError(name)
def get_for_call(self, name: str) ‑> CallHandler[~T]
Expand source code
def get_for_call(self, name: str) -> CallHandler[T]:
    logger.debug(f"ChoiceCallHandlerContainer: get_for_call {name}")
    for container in self.containers:
        try:
            return container.get_for_call(name)
        except KeyError:
            logger.debug(f"fail on {container.__class__.__qualname__}")
            pass

    raise KeyError(name)
class Initializer

The base class for initializers. Initializers are used by the interpreter to set up InterpreterContext instances immediately before a script is run. Initializers are considered to implement the CallHandler interface, even though they don't actually handle calls.

Expand source code
class Initializer(abc.ABC):
    """
    The base class for initializers. Initializers are used by the interpreter to set up `scrolls.interpreter.state.InterpreterContext` instances
    immediately before a script is run. Initializers are considered to implement the `CallHandler` interface, even though
    they don't actually handle calls.
    """

    @abc.abstractmethod
    def handle_call(self, context: AnyContextTV) -> None:
        """
        Initialize an `scrolls.interpreter.state.InterpreterContext` or subclass.
        """
        ...

    def __contains__(self, command_name: str) -> bool:
        return False

Ancestors

  • abc.ABC

Subclasses

Methods

def handle_call(self, context: ~AnyContextTV) ‑> None

Initialize an InterpreterContext or subclass.

Expand source code
@abc.abstractmethod
def handle_call(self, context: AnyContextTV) -> None:
    """
    Initialize an `scrolls.interpreter.state.InterpreterContext` or subclass.
    """
    ...
class MutableCallHandlerContainer (*args, **kwargs)

A mutable CallHandler container.

Expand source code
class MutableCallHandlerContainer(CallHandlerContainer[T], t.Protocol[T]):
    """
    A mutable `CallHandler` container.
    """

    def add(self, handler: CallHandler[T], name: str = "") -> None: ...

    """Add a call handler to this container.

    If `name` is not specified, then a unique name should be generated. The specific name generated is up to the
    implementor.
    """

    def remove(self, handler: t.Union[CallHandler[T], str]) -> None: ...

    """Remove a call handler from this container."""

Ancestors

Methods

def add(self, handler: CallHandler[~T], name: str = '') ‑> None
Expand source code
def add(self, handler: CallHandler[T], name: str = "") -> None: ...
def remove(self, handler: Union[CallHandler[~T], str]) ‑> None
Expand source code
def remove(self, handler: t.Union[CallHandler[T], str]) -> None: ...
class RuntimeCall (name: str, node: ASTNode, params: Sequence[str], collect_param: Optional[str])

A simple runtime call that is implemented by some Scrolls code.

Warning

Instances of this class are created automatically by RuntimeCallHandler.

Expand source code
@dataclasses.dataclass
class RuntimeCall:
    """
    A simple runtime call that is implemented by some Scrolls code.

    .. WARNING::
        Instances of this class are created automatically by `scrolls.interpreter.callhandler.RuntimeCallHandler`.
    """

    name: str
    """The name of the call."""

    node: ast.ASTNode
    """The statement node that should be run when this call is executed."""

    params: t.Sequence[str]
    """The names of the parameters, corresponding to the names of the local variables created when this call
    is executed.
    """

    collect_param: t.Optional[str]
    """The name of the collect parameter, if any. This will always be the last parameter, and will
    collect all extra arguments fed into this call and interpret them as a string vector. In other words, this is
    the `*args` parameter, for Scrolls.
    """

Class variables

var collect_param : Optional[str]

The name of the collect parameter, if any. This will always be the last parameter, and will collect all extra arguments fed into this call and interpret them as a string vector. In other words, this is the *args parameter, for Scrolls.

var name : str

The name of the call.

var nodeASTNode

The statement node that should be run when this call is executed.

var params : Sequence[str]

The names of the parameters, corresponding to the names of the local variables created when this call is executed.

class RuntimeCallHandler

A basic call handler that maps names to AST nodes.

Expand source code
class RuntimeCallHandler(t.Generic[T_co]):
    """
    A basic call handler that maps names to AST nodes.
    """

    def __init__(self) -> None:
        self.calls: t.MutableMapping[str, RuntimeCall] = {}

    def define(self, name: str, node: ast.ASTNode, params: t.Sequence[str]) -> None:
        """
        Defines a new call implemented with Scrolls code. See `RuntimeCall`.
        """
        collect_param: t.Optional[str] = None

        if params and params[-1].startswith("*"):
            collect_param = params[-1][1:]
            params = params[:-1]

        call = RuntimeCall(
            name,
            node,
            params,
            collect_param
        )

        self.calls[name] = call

    def undefine(self, name: str) -> None:
        """
        Delete a defined runtime call.
        """
        del self.calls[name]

    def handle_call(self, context: state.InterpreterContext) -> T_co:
        call = self.calls[context.call_name]

        # Arg length check
        if call.collect_param is None:
            if len(call.params) != len(context.args):
                raise interpreter_errors.InterpreterError(
                    context,
                    f"{context.call_name}: Invalid # of arguments (expected {len(call.params)})"
                )
        else:
            if len(context.args) < len(call.params) - 1:
                raise interpreter_errors.InterpreterError(
                    context,
                    f"{context.call_name}: Invalid # of arguments (expected at least {len(call.params)})"
                )

        params = list(call.params)

        if call.collect_param is None:
            args = context.args
        else:
            params.append(call.collect_param)
            collected = context.args[len(call.params):]
            args = list(context.args[:len(call.params)])
            args.append(" ".join(collected))

        # New scope must be created. We're running Scrolls code to implement this call, so it might trample
        # what's been defined otherwise. Plus, we don't want our call arguments to continue existing
        # after we're done.
        context.vars.new_scope()
        for param, arg in zip(params, args):
            context.set_var(param, arg)

        context.call_context.runtime_call = True
        try:
            # Interpret the body of the call.
            context.interpreter.interpret_statement(context, call.node)
        except interpreter_errors.InterpreterReturn:
            pass

        context.vars.destroy_scope()

        # TODO Fix typing here
        return t.cast(T_co, context.call_context.return_value)

    def __contains__(self, command_name: str) -> bool:
        return command_name in self.calls

Ancestors

  • typing.Generic

Methods

def define(self, name: str, node: ASTNode, params: Sequence[str]) ‑> None

Defines a new call implemented with Scrolls code. See RuntimeCall.

Expand source code
def define(self, name: str, node: ast.ASTNode, params: t.Sequence[str]) -> None:
    """
    Defines a new call implemented with Scrolls code. See `RuntimeCall`.
    """
    collect_param: t.Optional[str] = None

    if params and params[-1].startswith("*"):
        collect_param = params[-1][1:]
        params = params[:-1]

    call = RuntimeCall(
        name,
        node,
        params,
        collect_param
    )

    self.calls[name] = call
def handle_call(self, context: InterpreterContext) ‑> +T_co
Expand source code
def handle_call(self, context: state.InterpreterContext) -> T_co:
    call = self.calls[context.call_name]

    # Arg length check
    if call.collect_param is None:
        if len(call.params) != len(context.args):
            raise interpreter_errors.InterpreterError(
                context,
                f"{context.call_name}: Invalid # of arguments (expected {len(call.params)})"
            )
    else:
        if len(context.args) < len(call.params) - 1:
            raise interpreter_errors.InterpreterError(
                context,
                f"{context.call_name}: Invalid # of arguments (expected at least {len(call.params)})"
            )

    params = list(call.params)

    if call.collect_param is None:
        args = context.args
    else:
        params.append(call.collect_param)
        collected = context.args[len(call.params):]
        args = list(context.args[:len(call.params)])
        args.append(" ".join(collected))

    # New scope must be created. We're running Scrolls code to implement this call, so it might trample
    # what's been defined otherwise. Plus, we don't want our call arguments to continue existing
    # after we're done.
    context.vars.new_scope()
    for param, arg in zip(params, args):
        context.set_var(param, arg)

    context.call_context.runtime_call = True
    try:
        # Interpret the body of the call.
        context.interpreter.interpret_statement(context, call.node)
    except interpreter_errors.InterpreterReturn:
        pass

    context.vars.destroy_scope()

    # TODO Fix typing here
    return t.cast(T_co, context.call_context.return_value)
def undefine(self, name: str) ‑> None

Delete a defined runtime call.

Expand source code
def undefine(self, name: str) -> None:
    """
    Delete a defined runtime call.
    """
    del self.calls[name]
class ScrollCallback (*args, **kwargs)

Protocol for callbacks passed into CallbackCallHandler objects.

A ScrollCallback is any typing.Callable that takes an InterpreterContext or subclass as its only parameter.

Expand source code
class ScrollCallback(t.Protocol[T_co]):
    """
    Protocol for callbacks passed into `scrolls.interpreter.callhandler.CallbackCallHandler` objects.

    A `ScrollCallback` is any `typing.Callable` that takes an `scrolls.interpreter.state.InterpreterContext` or subclass as its only parameter.
    """

    def __call__(self, context: AnyContextTV) -> T_co: ...

Ancestors

  • typing.Protocol
  • typing.Generic