Module scrolls.ext.callbase
A basic call framework for Scrolls.
Takes care of the basic boilerplate for parsing options, and offers a slightly nicer decorator-based approach to defining new Scrolls calls.
Basic Usage
Here's an example that implements the print
builtin:
import typing
import scrolls
# Note that callbase is an extension, and is not part of the main scrolls namespace.
from scrolls.ext import callbase
interpreter = scrolls.Interpreter()
commands: callbase.CallBaseCallHandler[None] = callbase.CallBaseCallHandler()
interpreter.command_handlers.add(commands)
@commands.add_callbase
@callbase.Option(
"args",
type=str,
modifier=callbase.OptionModifier.GREEDY
)
@callbase.make_callbase(
"print",
"Prints all arguments."
)
def print_(ctx: scrolls.InterpreterContext, args: typing.Sequence[typing.Any]) -> None:
print(" ".join(args))
About
Callbase handles the job of parsing arguments, and allows some additional information
to be attached to Scrolls calls. Using callbase also means you do not need to implement
CallHandler
yourself in order to get started extending Scrolls.
None of the calls defined in scrolls.builtins
use this extension. It is solely for
external users.
Expand source code
"""
A basic call framework for Scrolls.
Takes care of the basic boilerplate for parsing options, and offers a slightly nicer decorator-based approach to
defining new Scrolls calls.
.. include:: ./pdoc/callbase.md
"""
import abc
import dataclasses
import enum
import logging
import numbers
import typing as t
import scrolls
__all__ = (
"rangelimit",
"make_callbase",
"set_help",
"OptionError",
"OptionChoiceError",
"OptionRangeLimitError",
"OptionConversionError",
"OptionRequiredMissingError",
"Option",
"OptionModifier",
"CallBase",
"CallbackCallBase",
"CallBaseCallHandler"
)
logger = logging.getLogger(__name__)
OptionTV = t.TypeVar("OptionTV", str, int, float, bool)
T = t.TypeVar("T")
T_co = t.TypeVar("T_co", covariant=True)
CallBaseCallbackReturnTV = t.TypeVar("CallBaseCallbackReturnTV")
CallBaseCallbackT = t.Callable[[scrolls.InterpreterContext, t.Sequence[t.Any]], CallBaseCallbackReturnTV]
def rangelimit(
node: scrolls.ASTNode,
low: t.Optional[numbers.Real],
val: numbers.Real,
high: t.Optional[numbers.Real]
) -> None:
"""
Check if val is between low and high. If not, raise a RangeLimitError.
"""
if low is None and high is None:
raise ValueError("low and high cannot both be None")
if not (__rangecheck_lower(low, val) and __rangecheck_upper(high, val)):
raise OptionRangeLimitError(node, low, val, high)
def __rangecheck_lower(
low: t.Optional[numbers.Real],
val: numbers.Real
) -> bool:
if low is None:
return True
return low <= val
def __rangecheck_upper(
high: t.Optional[numbers.Real],
val: numbers.Real
) -> bool:
if high is None:
return True
return val <= high
class OptionError(Exception):
"""
Base class for errors parsing arguments.
Args:
node: The `scrolls.ast.syntax.ASTNode` corresponding to the argument that failed to parse.
"""
def __init__(self, node: scrolls.ASTNode):
self.node = node
class OptionRangeLimitError(OptionError):
"""
Raised when a numeric argument (int, float) is outside the bounds set
by the `Option`.
"""
def __init__(
self,
node: scrolls.ASTNode,
low: t.Optional[numbers.Real],
val: numbers.Real,
high: t.Optional[numbers.Real]
):
super().__init__(node)
self.low = low
self.val = val
self.high = high
def __str__(self) -> str:
if self.high is None:
msg = f"cannot go below {self.low}"
elif self.low is None:
msg = f"cannot exceed {self.high}"
else:
msg = f"must be between {self.low} and {self.high}"
return msg
class OptionChoiceError(OptionError, t.Generic[OptionTV]):
"""
Raised when an argument is not one of the choices set by the `Option`.
"""
def __init__(
self,
node: scrolls.ASTNode,
bad_choice: OptionTV,
choices: t.Sequence[OptionTV]
):
super().__init__(node)
self.bad_choice: OptionTV = bad_choice
self.choices: t.Sequence[OptionTV] = choices
def __str__(self) -> str:
choices_str = ", ".join([str(choice) for choice in self.choices])
return f"has bad value '{self.bad_choice}', must be one of {choices_str}"
class OptionConversionError(OptionError):
"""
Raised when an option fails to convert to the type set by the `Option`.
"""
def __init__(
self,
node: scrolls.ASTNode,
cause: Exception
):
super().__init__(node)
self.cause = cause
def __str__(self) -> str:
return str(self.cause)
class OptionRequiredMissingError(Exception):
"""
Raised when required argument is missing.
"""
def __str__(self) -> str:
return "is a required argument that is missing"
class OptionModifier(enum.Enum):
"""
Enum describing different possible parsing methods used by callbase.
"""
NONE = 0
"""
The default. Only one argument is consumed.
"""
GREEDY = 1
"""
Arguments are consumed and added to a list until either arguments are exhausted,
or an argument fails to convert.
"""
CONSUME_REST = 2
"""
Consumes the rest of the passed arguments as a single string, joined by spaces.
If a CONSUME_REST `Option` is used:
- It must be the last `Option`.
- It must be of type `str`.
"""
class CallBaseCallHandler(scrolls.CallbackCallHandler[T_co]):
"""
The primary `scrolls.interpreter.callhandler.CallHandler` implementation for callbase.
Only `CallBase` subclasses may be added to this handler. See
[Basic Usage](#basic-usage)
"""
def __init__(self) -> None:
super().__init__()
self._consume_rest_triggers: t.MutableMapping[str, int] = {}
def add_call(self, name: str, call: scrolls.ScrollCallback[T_co]) -> None:
# Disallow non-callbase additions.
# This violates LSP, but the Scrolls core never calls add_call, so it's fine for now :beanos:
if not isinstance(call, CallBase):
raise TypeError("Calls added to CallBaseCallHandler must be a type deriving scrolls.commands.CallBase")
super().add_call(name, call)
# TODO add remove_callbase
def add_callbase(self, cmd: 'CallBase[T_co]') -> 'CallBase[T_co]':
"""
Add a `CallBase` instance to this handler. Automatically handles setting
up aliases and consume rest triggers. If you want to bypass this automatic
behavior, see `CallBaseCallHandler.add_call`.
"""
self.add_call(cmd.name, cmd)
for alias in cmd.aliases:
self.add_alias(alias, cmd.name)
# Set up consume_rest_triggers, which change the behavior of the tokenizer
# for commands that specify consume_rest.
if cmd.options and cmd.options[-1].modifier == OptionModifier.CONSUME_REST:
non_rest_count = len(cmd.options) - 1
self._consume_rest_triggers[cmd.name] = non_rest_count
for alias in cmd.aliases:
self._consume_rest_triggers[alias] = non_rest_count
return cmd
def all_callbase(self) -> t.Sequence['CallBase']:
"""
Get all `CallBase` instances added to this handler.
"""
# Safe to cast here, since adding non-callbase calls is not allowed.
return [t.cast(CallBase, cmd) for cmd in self.calls.values()]
@property
def consume_rest_triggers(self) -> t.Mapping[str, int]:
"""
Get the consume rest triggers for all `CallBase` instances attached
to this handler. This must be passed into the `scrolls.ast.tokenizer.Tokenizer`
used to tokenize a script. If using `scrolls.interpreter.run.Interpreter.run`,
this must be passed into the `consume_rest_triggers` argument.
"""
return self._consume_rest_triggers
class CallBase(abc.ABC, t.Generic[T_co]):
"""
The base `CallBase` class. To use, subclass and implement the `CallBase.invoke`
method.
Args:
name: The name of the call.
description: A short description of the call.
aliases: A sequence of alternate names for the call.
hidden: Hint to help displays that this call should be hidden.
"""
def __init__(
self,
name: str,
description: str,
aliases: t.Sequence[str] = (),
hidden: bool = False
):
self.name = name
self.description = description
self.help = ""
self.aliases = aliases
self.hidden = hidden
self.options: t.MutableSequence[Option] = []
def add_option(self, option: 'Option') -> None:
"""
Add an option to this callbase.
"""
if self.options and self.options[-1].modifier == OptionModifier.CONSUME_REST:
raise ValueError("Cannot add options after CONSUME_REST")
self.options.append(option)
def set_help(self, help: str) -> None:
"""
Set a long help text for this call.
"""
self.help = help
def convert_options(self, context: scrolls.InterpreterContext) -> t.Sequence[t.Any]:
"""
Using the `Option` instances added to this call, parse the arguments contained
in the passed context object.
"""
converted_opts = []
args = context.args
nodes = context.arg_nodes
idx = 0
for option in self.options:
if idx in nodes:
# Update current_node for error reporting purposes, but only if idx is pointing to
# a valid node. idx may be greater than the max if we're consuming a non-required
# option the user didn't specify.
context.current_node = nodes[idx]
try:
if option.modifier == OptionModifier.CONSUME_REST:
if not args:
raise OptionRequiredMissingError()
converted_opts.append(args[0])
break
_result, num_consumed = consume_option(option, args, nodes, idx)
except OptionRequiredMissingError as e:
raise scrolls.InterpreterError(
context,
f"{option.name} {e}"
)
except OptionError as e:
raise scrolls.InterpreterError(
context,
f"{option.name}: {e}"
)
if len(_result) == 1 and option.modifier != OptionModifier.GREEDY:
result = _result[0]
else:
result = _result
converted_opts.append(result)
args = args[num_consumed:]
idx += num_consumed
return converted_opts
@abc.abstractmethod
def invoke(self, context: scrolls.InterpreterContext, args: t.Sequence[t.Any]) -> T_co:
"""
Invoke this call.
"""
...
def __call__(self, context: scrolls.InterpreterContext) -> T_co:
return self.invoke(context, self.convert_options(context))
class CallbackCallBase(CallBase[CallBaseCallbackReturnTV]):
"""
A basic callbase that just invokes a given callback. Used for the make_callback_command decorator.
"""
def __init__(
self,
name: str,
description: str,
callback: CallBaseCallbackT,
aliases: t.Sequence[str] = (),
hidden: bool = False
):
super().__init__(name, description, aliases, hidden)
self.callback = callback
def invoke(self, context: scrolls.InterpreterContext, args: t.Sequence[t.Any]) -> CallBaseCallbackReturnTV:
# mypy complains Any is being returned here - odd, but I can confirm this is not the case, so cast it.
return t.cast(CallBaseCallbackReturnTV, self.callback(context, args))
def make_callbase(
name: str,
description: str,
aliases: t.Sequence[str] = (),
hidden: bool = False
) -> t.Callable[[CallBaseCallbackT], CallbackCallBase[CallBaseCallbackReturnTV]]:
"""
Decorator that creates a `CallbackCallBase` out of a function.
**Usage Example**
```py
import scrolls
from scrolls.ext import callbase
from typing import Sequence, Any
@callbase.make_callbase(
name="mycall",
description="This is an example.",
aliases=["mycall_alias", "foobar"]
)
def call(context: scrolls.InterpreterContext, args: Sequence[Any]) -> None:
# implementation here
...
```
See [Basic Usage](#basic-usage) for a more complete usage example.
Args:
name: The name of the call.
description: A short description of the call.
aliases: A sequence of alternate names for the call.
hidden: Hint to help displays that this call should be hidden.
"""
def decorate(callback: CallBaseCallbackT) -> CallbackCallBase[CallBaseCallbackReturnTV]:
return CallbackCallBase(
name,
description,
callback,
aliases,
hidden
)
return decorate
@dataclasses.dataclass
class Option(t.Generic[OptionTV]):
"""
An option for a `CallBase`. Controls parsing for arguments when a call is invoked.
Options may be added to a `CallBase` through `CallBase.add_option`, or used as
a decorator for convenience. See [Basic Usage](#basic-usage).
Args:
name: The name of the option.
type: A type to convert the str option to.
minimum: The minimum bound for this option, if type is `int` or `float`.
default: The default value for this option. If specified, this option is
not required.
maximum: The maximum bound for this option, if type is `int` or `float`.
modifier: Selects the option parsing behavior. See `OptionModifier`.
choices: Limits this option to a number of distinct choices.
"""
name: str
type: t.Type[OptionTV] = t.cast(t.Type[OptionTV], str) # T is allowed to be str, but mypy complains. Cast it.
minimum: t.Optional[numbers.Real] = None
default: t.Optional[OptionTV] = None
maximum: t.Optional[numbers.Real] = None
modifier: OptionModifier = OptionModifier.NONE
choices: t.Sequence[OptionTV] = ()
def verify_option(self) -> None:
"""
Verifies this option was constructed correctly. Raises a `TypeError` if not.
"""
bad_consume_rest = (
self.modifier == OptionModifier.CONSUME_REST and
not self.type == str
)
if bad_consume_rest:
raise TypeError("OptionModifier.CONSUME_REST can only be used on type str")
def __call__(self, cmd: CallBase) -> CallBase:
"""Enables Options to be used as decorators directly."""
self.verify_option()
cmd.add_option(self)
return cmd
def convert_arg(self, arg: str, node: scrolls.ASTNode) -> OptionTV:
"""
Convert a string to the type specified for this option, and check:
- `choices`
- `maximum`
- `minimum`
"""
logger.debug(f"Attempt to convert {arg} to {self.type.__name__}")
try:
arg_converted = self.type(arg)
except Exception as e:
raise OptionConversionError(node, e) from e
if isinstance(arg_converted, numbers.Real):
if self.minimum is not None or self.maximum is not None:
rangelimit(node, self.minimum, arg_converted, self.maximum)
if self.choices and arg_converted not in self.choices:
raise OptionChoiceError(node, arg_converted, self.choices)
return arg_converted
def consume_option(
option: Option[OptionTV],
args: t.Sequence[str],
nodes: scrolls.ArgSourceMap[scrolls.ASTNode],
arg_num: int
) -> tuple[t.Sequence[OptionTV], int]:
"""
Consume arguments from a list of strings according to the parsing rules
in `Option`.
Args:
option: The option to parse.
args: The arguments to consume from.
nodes: A map of argument numbers to source `scrolls.ast.ast_constants.ASTNode` instances.
arg_num: The arg number of `args[0]`.
Returns:
A tuple containing the sequence of converted options, and the number of
arguments consumed.
"""
logger.debug(f"consume_option: args={str(args)}")
idx = 0
if not args and option.default is not None:
logger.debug(f"{option.name}: return default arg {option.default}")
return [option.default], 1
elif not args:
raise OptionRequiredMissingError()
converted_args: t.MutableSequence[OptionTV] = []
while idx < len(args):
arg = args[idx]
node = nodes[arg_num + idx]
try:
converted_arg = option.convert_arg(arg, node)
if option.modifier == OptionModifier.NONE:
return [converted_arg], 1
converted_args.append(converted_arg)
idx += 1
except OptionError as e:
if option.modifier == OptionModifier.NONE:
logger.debug(f"Conversion failed.")
raise
else:
# For GREEDY, bail on first conversion failure.
logger.debug(f"GREEDY: Got {str(converted_args)}")
return converted_args, idx
logger.debug(f"GREEDY: Exhausted arguments. Got {str(converted_args)}")
return converted_args, idx
def set_help(
help: str
) -> t.Callable[[CallBase], CallBase]:
"""
Sets the long help text for a `CallBase` instance.
"""
def decorate(f: CallBase) -> CallBase:
f.set_help(help)
return f
return decorate
Functions
def make_callbase(name: str, description: str, aliases: Sequence[str] = (), hidden: bool = False) ‑> Callable[[Callable[[InterpreterContext, Sequence[Any]], ~CallBaseCallbackReturnTV]], CallbackCallBase[~CallBaseCallbackReturnTV]]
-
Decorator that creates a
CallbackCallBase
out of a function.Usage Example
import scrolls from scrolls.ext import callbase from typing import Sequence, Any @callbase.make_callbase( name="mycall", description="This is an example.", aliases=["mycall_alias", "foobar"] ) def call(context: scrolls.InterpreterContext, args: Sequence[Any]) -> None: # implementation here ...
See Basic Usage for a more complete usage example.
Args
name
- The name of the call.
description
- A short description of the call.
aliases
- A sequence of alternate names for the call.
hidden
- Hint to help displays that this call should be hidden.
Expand source code
def make_callbase( name: str, description: str, aliases: t.Sequence[str] = (), hidden: bool = False ) -> t.Callable[[CallBaseCallbackT], CallbackCallBase[CallBaseCallbackReturnTV]]: """ Decorator that creates a `CallbackCallBase` out of a function. **Usage Example** ```py import scrolls from scrolls.ext import callbase from typing import Sequence, Any @callbase.make_callbase( name="mycall", description="This is an example.", aliases=["mycall_alias", "foobar"] ) def call(context: scrolls.InterpreterContext, args: Sequence[Any]) -> None: # implementation here ... ``` See [Basic Usage](#basic-usage) for a more complete usage example. Args: name: The name of the call. description: A short description of the call. aliases: A sequence of alternate names for the call. hidden: Hint to help displays that this call should be hidden. """ def decorate(callback: CallBaseCallbackT) -> CallbackCallBase[CallBaseCallbackReturnTV]: return CallbackCallBase( name, description, callback, aliases, hidden ) return decorate
def rangelimit(node: ASTNode, low: Optional[numbers.Real], val: numbers.Real, high: Optional[numbers.Real]) ‑> None
-
Check if val is between low and high. If not, raise a RangeLimitError.
Expand source code
def rangelimit( node: scrolls.ASTNode, low: t.Optional[numbers.Real], val: numbers.Real, high: t.Optional[numbers.Real] ) -> None: """ Check if val is between low and high. If not, raise a RangeLimitError. """ if low is None and high is None: raise ValueError("low and high cannot both be None") if not (__rangecheck_lower(low, val) and __rangecheck_upper(high, val)): raise OptionRangeLimitError(node, low, val, high)
def set_help(help: str) ‑> Callable[[CallBase], CallBase]
-
Sets the long help text for a
CallBase
instance.Expand source code
def set_help( help: str ) -> t.Callable[[CallBase], CallBase]: """ Sets the long help text for a `CallBase` instance. """ def decorate(f: CallBase) -> CallBase: f.set_help(help) return f return decorate
Classes
class CallBase (name: str, description: str, aliases: Sequence[str] = (), hidden: bool = False)
-
The base
CallBase
class. To use, subclass and implement theCallBase.invoke()
method.Args
name
- The name of the call.
description
- A short description of the call.
aliases
- A sequence of alternate names for the call.
hidden
- Hint to help displays that this call should be hidden.
Expand source code
class CallBase(abc.ABC, t.Generic[T_co]): """ The base `CallBase` class. To use, subclass and implement the `CallBase.invoke` method. Args: name: The name of the call. description: A short description of the call. aliases: A sequence of alternate names for the call. hidden: Hint to help displays that this call should be hidden. """ def __init__( self, name: str, description: str, aliases: t.Sequence[str] = (), hidden: bool = False ): self.name = name self.description = description self.help = "" self.aliases = aliases self.hidden = hidden self.options: t.MutableSequence[Option] = [] def add_option(self, option: 'Option') -> None: """ Add an option to this callbase. """ if self.options and self.options[-1].modifier == OptionModifier.CONSUME_REST: raise ValueError("Cannot add options after CONSUME_REST") self.options.append(option) def set_help(self, help: str) -> None: """ Set a long help text for this call. """ self.help = help def convert_options(self, context: scrolls.InterpreterContext) -> t.Sequence[t.Any]: """ Using the `Option` instances added to this call, parse the arguments contained in the passed context object. """ converted_opts = [] args = context.args nodes = context.arg_nodes idx = 0 for option in self.options: if idx in nodes: # Update current_node for error reporting purposes, but only if idx is pointing to # a valid node. idx may be greater than the max if we're consuming a non-required # option the user didn't specify. context.current_node = nodes[idx] try: if option.modifier == OptionModifier.CONSUME_REST: if not args: raise OptionRequiredMissingError() converted_opts.append(args[0]) break _result, num_consumed = consume_option(option, args, nodes, idx) except OptionRequiredMissingError as e: raise scrolls.InterpreterError( context, f"{option.name} {e}" ) except OptionError as e: raise scrolls.InterpreterError( context, f"{option.name}: {e}" ) if len(_result) == 1 and option.modifier != OptionModifier.GREEDY: result = _result[0] else: result = _result converted_opts.append(result) args = args[num_consumed:] idx += num_consumed return converted_opts @abc.abstractmethod def invoke(self, context: scrolls.InterpreterContext, args: t.Sequence[t.Any]) -> T_co: """ Invoke this call. """ ... def __call__(self, context: scrolls.InterpreterContext) -> T_co: return self.invoke(context, self.convert_options(context))
Ancestors
- abc.ABC
- typing.Generic
Subclasses
Methods
def add_option(self, option: Option) ‑> None
-
Add an option to this callbase.
Expand source code
def add_option(self, option: 'Option') -> None: """ Add an option to this callbase. """ if self.options and self.options[-1].modifier == OptionModifier.CONSUME_REST: raise ValueError("Cannot add options after CONSUME_REST") self.options.append(option)
def convert_options(self, context: InterpreterContext) ‑> Sequence[Any]
-
Using the
Option
instances added to this call, parse the arguments contained in the passed context object.Expand source code
def convert_options(self, context: scrolls.InterpreterContext) -> t.Sequence[t.Any]: """ Using the `Option` instances added to this call, parse the arguments contained in the passed context object. """ converted_opts = [] args = context.args nodes = context.arg_nodes idx = 0 for option in self.options: if idx in nodes: # Update current_node for error reporting purposes, but only if idx is pointing to # a valid node. idx may be greater than the max if we're consuming a non-required # option the user didn't specify. context.current_node = nodes[idx] try: if option.modifier == OptionModifier.CONSUME_REST: if not args: raise OptionRequiredMissingError() converted_opts.append(args[0]) break _result, num_consumed = consume_option(option, args, nodes, idx) except OptionRequiredMissingError as e: raise scrolls.InterpreterError( context, f"{option.name} {e}" ) except OptionError as e: raise scrolls.InterpreterError( context, f"{option.name}: {e}" ) if len(_result) == 1 and option.modifier != OptionModifier.GREEDY: result = _result[0] else: result = _result converted_opts.append(result) args = args[num_consumed:] idx += num_consumed return converted_opts
def invoke(self, context: InterpreterContext, args: Sequence[Any]) ‑> +T_co
-
Invoke this call.
Expand source code
@abc.abstractmethod def invoke(self, context: scrolls.InterpreterContext, args: t.Sequence[t.Any]) -> T_co: """ Invoke this call. """ ...
def set_help(self, help: str) ‑> None
-
Set a long help text for this call.
Expand source code
def set_help(self, help: str) -> None: """ Set a long help text for this call. """ self.help = help
class CallBaseCallHandler
-
The primary
CallHandler
implementation for callbase. OnlyCallBase
subclasses may be added to this handler. See Basic UsageExpand source code
class CallBaseCallHandler(scrolls.CallbackCallHandler[T_co]): """ The primary `scrolls.interpreter.callhandler.CallHandler` implementation for callbase. Only `CallBase` subclasses may be added to this handler. See [Basic Usage](#basic-usage) """ def __init__(self) -> None: super().__init__() self._consume_rest_triggers: t.MutableMapping[str, int] = {} def add_call(self, name: str, call: scrolls.ScrollCallback[T_co]) -> None: # Disallow non-callbase additions. # This violates LSP, but the Scrolls core never calls add_call, so it's fine for now :beanos: if not isinstance(call, CallBase): raise TypeError("Calls added to CallBaseCallHandler must be a type deriving scrolls.commands.CallBase") super().add_call(name, call) # TODO add remove_callbase def add_callbase(self, cmd: 'CallBase[T_co]') -> 'CallBase[T_co]': """ Add a `CallBase` instance to this handler. Automatically handles setting up aliases and consume rest triggers. If you want to bypass this automatic behavior, see `CallBaseCallHandler.add_call`. """ self.add_call(cmd.name, cmd) for alias in cmd.aliases: self.add_alias(alias, cmd.name) # Set up consume_rest_triggers, which change the behavior of the tokenizer # for commands that specify consume_rest. if cmd.options and cmd.options[-1].modifier == OptionModifier.CONSUME_REST: non_rest_count = len(cmd.options) - 1 self._consume_rest_triggers[cmd.name] = non_rest_count for alias in cmd.aliases: self._consume_rest_triggers[alias] = non_rest_count return cmd def all_callbase(self) -> t.Sequence['CallBase']: """ Get all `CallBase` instances added to this handler. """ # Safe to cast here, since adding non-callbase calls is not allowed. return [t.cast(CallBase, cmd) for cmd in self.calls.values()] @property def consume_rest_triggers(self) -> t.Mapping[str, int]: """ Get the consume rest triggers for all `CallBase` instances attached to this handler. This must be passed into the `scrolls.ast.tokenizer.Tokenizer` used to tokenize a script. If using `scrolls.interpreter.run.Interpreter.run`, this must be passed into the `consume_rest_triggers` argument. """ return self._consume_rest_triggers
Ancestors
- CallbackCallHandler
- typing.Generic
Instance variables
var consume_rest_triggers : Mapping[str, int]
-
Get the consume rest triggers for all
CallBase
instances attached to this handler. This must be passed into theTokenizer
used to tokenize a script. If usingInterpreter.run()
, this must be passed into theconsume_rest_triggers
argument.Expand source code
@property def consume_rest_triggers(self) -> t.Mapping[str, int]: """ Get the consume rest triggers for all `CallBase` instances attached to this handler. This must be passed into the `scrolls.ast.tokenizer.Tokenizer` used to tokenize a script. If using `scrolls.interpreter.run.Interpreter.run`, this must be passed into the `consume_rest_triggers` argument. """ return self._consume_rest_triggers
Methods
def add_callbase(self, cmd: CallBase[T_co]) ‑> CallBase[+T_co]
-
Add a
CallBase
instance to this handler. Automatically handles setting up aliases and consume rest triggers. If you want to bypass this automatic behavior, seeCallBaseCallHandler.add_call()
.Expand source code
def add_callbase(self, cmd: 'CallBase[T_co]') -> 'CallBase[T_co]': """ Add a `CallBase` instance to this handler. Automatically handles setting up aliases and consume rest triggers. If you want to bypass this automatic behavior, see `CallBaseCallHandler.add_call`. """ self.add_call(cmd.name, cmd) for alias in cmd.aliases: self.add_alias(alias, cmd.name) # Set up consume_rest_triggers, which change the behavior of the tokenizer # for commands that specify consume_rest. if cmd.options and cmd.options[-1].modifier == OptionModifier.CONSUME_REST: non_rest_count = len(cmd.options) - 1 self._consume_rest_triggers[cmd.name] = non_rest_count for alias in cmd.aliases: self._consume_rest_triggers[alias] = non_rest_count return cmd
def all_callbase(self) ‑> Sequence[CallBase]
-
Get all
CallBase
instances added to this handler.Expand source code
def all_callbase(self) -> t.Sequence['CallBase']: """ Get all `CallBase` instances added to this handler. """ # Safe to cast here, since adding non-callbase calls is not allowed. return [t.cast(CallBase, cmd) for cmd in self.calls.values()]
Inherited members
class CallbackCallBase (name: str, description: str, callback: Callable[[InterpreterContext, Sequence[Any]], ~CallBaseCallbackReturnTV], aliases: Sequence[str] = (), hidden: bool = False)
-
A basic callbase that just invokes a given callback. Used for the make_callback_command decorator.
Expand source code
class CallbackCallBase(CallBase[CallBaseCallbackReturnTV]): """ A basic callbase that just invokes a given callback. Used for the make_callback_command decorator. """ def __init__( self, name: str, description: str, callback: CallBaseCallbackT, aliases: t.Sequence[str] = (), hidden: bool = False ): super().__init__(name, description, aliases, hidden) self.callback = callback def invoke(self, context: scrolls.InterpreterContext, args: t.Sequence[t.Any]) -> CallBaseCallbackReturnTV: # mypy complains Any is being returned here - odd, but I can confirm this is not the case, so cast it. return t.cast(CallBaseCallbackReturnTV, self.callback(context, args))
Ancestors
- CallBase
- abc.ABC
- typing.Generic
Inherited members
class Option (name: str, type: Type[~OptionTV] = builtins.str, minimum: Optional[numbers.Real] = None, default: Optional[~OptionTV] = None, maximum: Optional[numbers.Real] = None, modifier: OptionModifier = OptionModifier.NONE, choices: Sequence[~OptionTV] = ())
-
An option for a
CallBase
. Controls parsing for arguments when a call is invoked. Options may be added to aCallBase
throughCallBase.add_option()
, or used as a decorator for convenience. See Basic Usage.Args
name
- The name of the option.
type
- A type to convert the str option to.
minimum
- The minimum bound for this option, if type is
int
orfloat
. default
- The default value for this option. If specified, this option is not required.
maximum
- The maximum bound for this option, if type is
int
orfloat
. modifier
- Selects the option parsing behavior. See
OptionModifier
. choices
- Limits this option to a number of distinct choices.
Expand source code
@dataclasses.dataclass class Option(t.Generic[OptionTV]): """ An option for a `CallBase`. Controls parsing for arguments when a call is invoked. Options may be added to a `CallBase` through `CallBase.add_option`, or used as a decorator for convenience. See [Basic Usage](#basic-usage). Args: name: The name of the option. type: A type to convert the str option to. minimum: The minimum bound for this option, if type is `int` or `float`. default: The default value for this option. If specified, this option is not required. maximum: The maximum bound for this option, if type is `int` or `float`. modifier: Selects the option parsing behavior. See `OptionModifier`. choices: Limits this option to a number of distinct choices. """ name: str type: t.Type[OptionTV] = t.cast(t.Type[OptionTV], str) # T is allowed to be str, but mypy complains. Cast it. minimum: t.Optional[numbers.Real] = None default: t.Optional[OptionTV] = None maximum: t.Optional[numbers.Real] = None modifier: OptionModifier = OptionModifier.NONE choices: t.Sequence[OptionTV] = () def verify_option(self) -> None: """ Verifies this option was constructed correctly. Raises a `TypeError` if not. """ bad_consume_rest = ( self.modifier == OptionModifier.CONSUME_REST and not self.type == str ) if bad_consume_rest: raise TypeError("OptionModifier.CONSUME_REST can only be used on type str") def __call__(self, cmd: CallBase) -> CallBase: """Enables Options to be used as decorators directly.""" self.verify_option() cmd.add_option(self) return cmd def convert_arg(self, arg: str, node: scrolls.ASTNode) -> OptionTV: """ Convert a string to the type specified for this option, and check: - `choices` - `maximum` - `minimum` """ logger.debug(f"Attempt to convert {arg} to {self.type.__name__}") try: arg_converted = self.type(arg) except Exception as e: raise OptionConversionError(node, e) from e if isinstance(arg_converted, numbers.Real): if self.minimum is not None or self.maximum is not None: rangelimit(node, self.minimum, arg_converted, self.maximum) if self.choices and arg_converted not in self.choices: raise OptionChoiceError(node, arg_converted, self.choices) return arg_converted
Ancestors
- typing.Generic
Class variables
var choices : Sequence[~OptionTV]
var default : Optional[~OptionTV]
var maximum : Optional[numbers.Real]
var minimum : Optional[numbers.Real]
var modifier : OptionModifier
var name : str
var type : Type[~OptionTV]
-
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.
Methods
def convert_arg(self, arg: str, node: ASTNode) ‑> ~OptionTV
-
Convert a string to the type specified for this option, and check:
choices
maximum
minimum
Expand source code
def convert_arg(self, arg: str, node: scrolls.ASTNode) -> OptionTV: """ Convert a string to the type specified for this option, and check: - `choices` - `maximum` - `minimum` """ logger.debug(f"Attempt to convert {arg} to {self.type.__name__}") try: arg_converted = self.type(arg) except Exception as e: raise OptionConversionError(node, e) from e if isinstance(arg_converted, numbers.Real): if self.minimum is not None or self.maximum is not None: rangelimit(node, self.minimum, arg_converted, self.maximum) if self.choices and arg_converted not in self.choices: raise OptionChoiceError(node, arg_converted, self.choices) return arg_converted
def verify_option(self) ‑> None
-
Verifies this option was constructed correctly. Raises a
TypeError
if not.Expand source code
def verify_option(self) -> None: """ Verifies this option was constructed correctly. Raises a `TypeError` if not. """ bad_consume_rest = ( self.modifier == OptionModifier.CONSUME_REST and not self.type == str ) if bad_consume_rest: raise TypeError("OptionModifier.CONSUME_REST can only be used on type str")
class OptionChoiceError (node: ASTNode, bad_choice: ~OptionTV, choices: Sequence[~OptionTV])
-
Raised when an argument is not one of the choices set by the
Option
.Expand source code
class OptionChoiceError(OptionError, t.Generic[OptionTV]): """ Raised when an argument is not one of the choices set by the `Option`. """ def __init__( self, node: scrolls.ASTNode, bad_choice: OptionTV, choices: t.Sequence[OptionTV] ): super().__init__(node) self.bad_choice: OptionTV = bad_choice self.choices: t.Sequence[OptionTV] = choices def __str__(self) -> str: choices_str = ", ".join([str(choice) for choice in self.choices]) return f"has bad value '{self.bad_choice}', must be one of {choices_str}"
Ancestors
- OptionError
- builtins.Exception
- builtins.BaseException
- typing.Generic
class OptionConversionError (node: ASTNode, cause: Exception)
-
Raised when an option fails to convert to the type set by the
Option
.Expand source code
class OptionConversionError(OptionError): """ Raised when an option fails to convert to the type set by the `Option`. """ def __init__( self, node: scrolls.ASTNode, cause: Exception ): super().__init__(node) self.cause = cause def __str__(self) -> str: return str(self.cause)
Ancestors
- OptionError
- builtins.Exception
- builtins.BaseException
class OptionError (node: ASTNode)
-
Base class for errors parsing arguments.
Args
node
- The
ASTNode
corresponding to the argument that failed to parse.
Expand source code
class OptionError(Exception): """ Base class for errors parsing arguments. Args: node: The `scrolls.ast.syntax.ASTNode` corresponding to the argument that failed to parse. """ def __init__(self, node: scrolls.ASTNode): self.node = node
Ancestors
- builtins.Exception
- builtins.BaseException
Subclasses
class OptionModifier (value, names=None, *, module=None, qualname=None, type=None, start=1)
-
Enum describing different possible parsing methods used by callbase.
Expand source code
class OptionModifier(enum.Enum): """ Enum describing different possible parsing methods used by callbase. """ NONE = 0 """ The default. Only one argument is consumed. """ GREEDY = 1 """ Arguments are consumed and added to a list until either arguments are exhausted, or an argument fails to convert. """ CONSUME_REST = 2 """ Consumes the rest of the passed arguments as a single string, joined by spaces. If a CONSUME_REST `Option` is used: - It must be the last `Option`. - It must be of type `str`. """
Ancestors
- enum.Enum
Class variables
var CONSUME_REST
var GREEDY
-
Arguments are consumed and added to a list until either arguments are exhausted, or an argument fails to convert.
var NONE
-
The default. Only one argument is consumed.
class OptionRangeLimitError (node: ASTNode, low: Optional[numbers.Real], val: numbers.Real, high: Optional[numbers.Real])
-
Raised when a numeric argument (int, float) is outside the bounds set by the
Option
.Expand source code
class OptionRangeLimitError(OptionError): """ Raised when a numeric argument (int, float) is outside the bounds set by the `Option`. """ def __init__( self, node: scrolls.ASTNode, low: t.Optional[numbers.Real], val: numbers.Real, high: t.Optional[numbers.Real] ): super().__init__(node) self.low = low self.val = val self.high = high def __str__(self) -> str: if self.high is None: msg = f"cannot go below {self.low}" elif self.low is None: msg = f"cannot exceed {self.high}" else: msg = f"must be between {self.low} and {self.high}" return msg
Ancestors
- OptionError
- builtins.Exception
- builtins.BaseException
class OptionRequiredMissingError (*args, **kwargs)
-
Raised when required argument is missing.
Expand source code
class OptionRequiredMissingError(Exception): """ Raised when required argument is missing. """ def __str__(self) -> str: return "is a required argument that is missing"
Ancestors
- builtins.Exception
- builtins.BaseException