Command System
The ZwyLib command registration system allows you to easily register commands, subcommands, and error handlers in just a few lines — and also dynamically add or remove them at runtime.
Getting Started
Let’s register a basic command:
# ... metadata and zwylib import ...
from zwylib import command_manager
def register_commands():
prefix = "!" # command prefix for your plugin
commands_priority = 10 # your commands' execution priority over others
# commands are registered through a dispatcher
dispatcher = command_manager.get_dispatcher(__id__, prefix, commands_priority)
# register the "!test" command
@dispatcher.register_command("test")
def test_command(params: Any, account: int) -> HookResult:
# https://plugins.exteragram.app/docs/plugin-class#message-sending-hook
params.message = "Command '!test' executed successfully!"
return HookResult(strategy=HookStrategy.MODIFY_FINAL, params=params)
class MyPlugin(BasePlugin):
def on_plugin_load(self):
# register commands when the plugin loads
register_commands()
def on_plugin_unload(self):
# on unload, deregister commands to avoid issues with plugin updates/validation
command_manager.remove_dispatcher(__id__)
... # rest of plugin logicThe arguments params and account are mandatory — ZwyLib will raise a MissingRequiredArguments error if these are missing.
ZwyLib also enforces the return type to be HookResult. If a different type is returned, an InvalidTypeError will be thrown and the command won’t be registered.
Aliases
Both register_command and subcommand accept an optional aliases list, letting a command respond to more than one name:
@dispatcher.register_command("test", aliases=["t", "tst"])
def test_command(params: Any, account: int) -> HookResult:
...Calling !test, !t, or !tst will all invoke test_command. Names and aliases share the same namespace — ZwyLib will refuse to register a name or alias that’s already taken by another command.
Subcommands
ZwyLib allows you to register as many nested subcommands as you like:
# ... metadata and zwylib import ...
def register_commands():
dispatcher = zwylib.command_manager.get_dispatcher(__id__, "!")
# called as "!test"
@dispatcher.register_command("test")
def test_command(params: Any, account: int) -> HookResult:
...
# called as "!test sub"
@test_command.subcommand("sub")
def test_subcommand(params: Any, account: int) -> HookResult:
params.message = "Command '!test sub' executed successfully!"
return HookResult(strategy=HookStrategy.MODIFY_FINAL, params=params)
# called as "!test sub new"
@test_subcommand.subcommand("new")
def test_sub_new_command(params: Any, account: int) -> HookResult:
params.message = "Command '!test sub new' executed successfully!"
return HookResult(strategy=HookStrategy.MODIFY_FINAL, params=params)Stub Commands
Sometimes you only want a command to act as a namespace for its subcommands, with no behavior of its own. Use register_stub_command (on a Dispatcher) or stub_subcommand (on a _Command) instead of providing a function — invoking the stub directly shows a hint bulletin instead of doing nothing silently:
dispatcher = zwylib.command_manager.get_dispatcher(__id__, "!")
# "!settings" has no behavior of its own...
settings_cmd = dispatcher.register_stub_command("settings")
# ...but "!settings reset" does
@settings_cmd.subcommand("reset")
def settings_reset(params: Any, account: int) -> HookResult:
params.message = "Settings were reset."
return HookResult(strategy=HookStrategy.MODIFY_FINAL, params=params)Arguments
ZwyLib automatically parses the message text and attempts to match parameters based on function arguments.
The function must have required params and account parameters and if a command function includes additional typed parameters, ZwyLib will try to parse and cast arguments to the expected types. Supported types include: str, int, float, bool, and generic Any, Union, Optional from the typing module (see Python typing documentation ).
Note: For boolean conversion, values like
true,1,yes,onmap toTrue, andfalse,0,no,offmap toFalse.
If casting fails, a CannotCastError is raised. If the number of provided arguments is less than the required (non-Optional, non-default, non-variadic) arguments or more than the expected arguments (when no variadic arguments are present), a WrongArgumentAmountError is raised. Arguments annotated as Optional[T] (or Union[T, None]) or with a default value (e.g., arg: str = None) are automatically assigned None or their default value if no value is provided.
ZwyLib also supports variadic arguments (*args), which must be annotated as *args: T, where T is one of the supported types (str, int, float, bool, Any, or a Union of these types). Variadic arguments are passed as a tuple to the command function:
- If no extra arguments are provided,
*argsis an empty tuple(). - If one extra argument is provided,
*argsis a single-item tuple(arg,). - If multiple extra arguments are provided,
*argsis a tuple of all extra arguments(arg1, arg2, ...).
Examples
Example 1: Required and Variadic Arguments
from typing import Union
def register_commands():
dispatcher = zwylib.command_manager.get_dispatcher(...)
@dispatcher.register_command("numbers")
def numbers_command(params: Any, account: int, first: int, *args: int) -> HookResult:
params.message = f"First: {first}, additional numbers: {args}"
return HookResult(strategy=HookStrategy.MODIFY_FINAL, params=params)!numbers 42→first = 42,args = ()→ Output:First: 42, additional numbers: ()!numbers 42 100→first = 42,args = (100,)→ Output:First: 42, additional numbers: (100,)!numbers 42 100 200 300→first = 42,args = (100, 200, 300)→ Output:First: 42, additional numbers: (100, 200, 300)!numbers→ Error:Expected at least 3 arguments, got 2
Example 2: Optional Argument
from typing import Optional
def register_commands():
dispatcher = zwylib.command_manager.get_dispatcher(...)
@dispatcher.register_command("test")
def test_command(params: Any, account: int, option: Optional[str]) -> HookResult:
params.message = f"Option: {option}"
return HookResult(strategy=HookStrategy.MODIFY_FINAL, params=params)!test hello 123→account = 123,option = None→ Output:Option: None!test hello 123 abc→account = 123,option = "abc"→ Output:Option: abc!test hello→ Error:Expected at least 2 arguments, got 1!test hello 123 abc def→ Error:Expected at most 3 arguments, got 4
Example 3: Optional Argument with Default Value
from typing import Optional
def register_commands():
dispatcher = zwylib.command_manager.get_dispatcher(...)
@dispatcher.register_command("test")
def test_command(params: Any, account: int, option: Optional[str] = None) -> HookResult:
params.message = f"Option: {option}"
return HookResult(strategy=HookStrategy.MODIFY_FINAL, params=params)!test hello 123→account = 123,option = None→ Output:Option: None!test hello 123 abc→account = 123,option = "abc"→ Output:Option: abc!test hello→ Error:Expected at least 2 arguments, got 1!test hello 123 abc def→ Error:Expected at most 3 arguments, got 4
Example 4: Only Variadic Arguments
from typing import Union
def register_commands():
dispatcher = zwylib.command_manager.get_dispatcher(...)
@dispatcher.register_command("echo")
def echo_command(params: Any, account: int, *args: Union[str, int]) -> HookResult:
params.message = f"Echo: {list(args)}"
return HookResult(strategy=HookStrategy.MODIFY_FINAL, params=params)!echo→args = ()→ Output:Echo: []!echo hello→args = ('hello',)→ Output:Echo: ['hello']!echo hello 42→args = ('hello', 42)→ Output:Echo: ['hello', 42]
If the *args parameter’s type or any argument type is not one of the supported types or a valid Union/Optional of supported types, an InvalidTypeError is raised during command registration.
Async Commands
Command functions may be defined with async def. Async commands must not return a HookResult — their return annotation must be omitted or None, since ZwyLib immediately cancels the outgoing message (HookStrategy.CANCEL) and schedules the coroutine on zwylib.async_manager instead of waiting for it:
from zwylib import Requests, build_bulletin_helper
@dispatcher.register_command("ping")
async def ping_command(params: Any, account: int):
response = await Requests.async_send(...)
build_bulletin_helper(__id__).show_info(f"Got: {response}")An error handler registered for an async command may itself be sync or async — ZwyLib schedules it the same way if it’s a coroutine function.
Error Handling
If an exception occurs during command or subcommand execution, it can be caught using the @command.register_error_handler decorator:
def register_commands():
dispatcher = zwylib.command_manager.get_dispatcher(...)
@dispatcher.register_command("number")
def number_command(params: Any, account: int, number: int) -> HookResult:
params.message = f"number: {type(number)}"
return HookResult(strategy=HookStrategy.MODIFY_FINAL, params=params)
@number_command.register_error_handler
def number_command_error_handler(params: Any, account: int, error: Exception) -> HookResult:
params.message = f"An error occurred in 'number': {error}"
return HookResult(strategy=HookStrategy.MODIFY_FINAL, params=params)The error handler must accept exactly three arguments: params, account, and error. Otherwise, ZwyLib won’t register the handler.
Unhandled exceptions in a command will cause ZwyLib to send the stack trace to chat.
Command Deregistration
To manually remove a command, use:
dispatcher = zwylib.command_manager.get_dispatcher(__id__)
dispatcher.unregister_command("my_command")This will also remove all subcommands associated with the removed command.
zwylib.CommandManager
zwylib.command_manager: CommandManagerThis global object is created during ZwyLib initialization and is used to manage all dispatchers. You should only use its documented methods.
Methods
get_dispatcher
CommandManager.get_dispatcher(
plugin_id: Optional[str] = None,
prefix="default", # defaults to "."
commands_priority=-1
) -> DispatcherCreates (if necessary) and returns a Dispatcher instance for the given plugin_id.
Parameters
plugin_id(Optional[str]): Your plugin’s unique ID. If omitted (orNone), ZwyLib infers it from the call stack of the caller’s plugin file.prefix(str): Prefix for all commands of this plugin."default"means".". Ignored if a prefix was previously saved for this plugin viaset_prefix.commands_priority(int): Execution priority. Default is-1.
Example
zwylib.command_manager.get_dispatcher("MyPluginID", "!", 10)remove_dispatcher
CommandManager.remove_dispatcher(plugin_id: Optional[str])Removes the dispatcher associated with the given plugin.
Parameters
plugin_id(Optional[str]): ID of the plugin whose dispatcher is being removed. PassNoneto infer it from the caller’s plugin file.
Example
zwylib.command_manager.remove_dispatcher(__id__)sort_dispatchers
CommandManager.sort_dispatchers()Re-sorts all registered dispatchers by their commands_priority, highest first. Called automatically whenever a new dispatcher is created — you shouldn’t normally need to call this yourself.
zwylib.Dispatcher
zwylib.command_manager.get_dispatcher(__id__): DispatcherA class returned by zwylib.command_manager.get_dispatcher, responsible for registering commands under the current plugin ID. Should only be obtained via get_dispatcher.
Methods
set_prefix
Dispatcher.set_prefix(prefix: str)Sets the prefix for all commands registered via this dispatcher. The prefix saves between exteraGram sessions. Fails silently (with a log entry) if prefix is empty or contains spaces.
Parameters
prefix(str): New command prefix.
Example
dispatcher.set_prefix("/")register_command
@dispatcher.register_command(name: str, aliases: List[str] = None)Decorator to register a command.
Arguments params and account are required. The return type must be HookResult, unless the command function is a coroutine function — see Async Commands.
Parameters
name(str): Command name. Cannot be empty or contain spaces.aliases(List[str], optional): Additional names that also invoke this command.
Raises
MissingRequiredArguments: Ifparamsoraccountare missing.InvalidTypeError: If parameter types are unsupported or return type is notHookResult(for sync commands).
Example
@dispatcher.register_command("hello", aliases=["hi"])
def test_command(params: Any, account: int) -> HookResult:
params.message = "Hi!"
return HookResult(strategy=HookStrategy.MODIFY, params=params)register_stub_command
Dispatcher.register_stub_command(name: str) -> _CommandRegisters a command with no behavior of its own, meant purely as a namespace for subcommands. Invoking it directly shows an informational bulletin instead of doing nothing. See Stub Commands.
Parameters
name(str): Command name.
Example
settings_cmd = dispatcher.register_stub_command("settings")unregister_command
Dispatcher.unregister_command(name: str)Removes a previously registered command (and all of its subcommands) from this dispatcher.
Parameters
name(str): Name of the command to remove.
Example
dispatcher.unregister_command("my_command")zwylib._Command
@dispatcher.register_command(...): _CommandThe object returned by register_command, representing a single registered command. Used to attach subcommands and error handlers to it.
Methods
subcommand
@command.subcommand(name: str, aliases: List[str] = None)Decorator to register a nested subcommand under this command. Follows the same argument and return-type rules as register_command.
Parameters
name(str): Subcommand name. Cannot be empty or contain spaces.aliases(List[str], optional): Additional names that also invoke this subcommand.
Example
@test_command.subcommand("sub")
def test_subcommand(params: Any, account: int) -> HookResult:
...stub_subcommand
_Command.stub_subcommand(name: str) -> _CommandRegisters a subcommand with no behavior of its own, meant purely as a namespace for further nested subcommands. See Stub Commands.
Parameters
name(str): Subcommand name.
Example
group_cmd = test_command.stub_subcommand("group")register_error_handler
@command.register_error_handlerDecorator to register an exception handler for this command (and any of its subcommands that don’t define their own). See Error Handling.
Raises
MissingRequiredArguments: If the handler doesn’t accept exactlyparams,account, anderror.
Example
@test_command.register_error_handler
def on_error(params: Any, account: int, error: Exception) -> HookResult:
params.message = f"Error: {error}"
return HookResult(strategy=HookStrategy.MODIFY_FINAL, params=params)