Skip to main content

dyncommands

Dynamic command execution, parsing, and storage.


Tests Codecov MIT License

PyPI Python CPython


About:

Dyncommands allows you to dynamically import and run python functions. Useful for adding commands to IRC chatbots or CLI applications without a restart.

When parsing a string, it separates the command name from arguments, and executes the stored function with those arguments. Each time the parser is called, you can pass in your own custom kwargs that the command will have access to.

All command modules are compiled through RestrictedPython before being allowed to run. You can turn off Restricted execution by setting CommandParser._unrestricted to true, though this is highly discouraged when running untrusted code.

How to use:

Short example:

from pathlib import Path
from dyncommands import CommandParser, CommandContext, CommandSource

output: str = ''

def callback(text, *args):
    global output
    output = text

path = Path('path/to/directory')  # Must be a directory with a commands.json file in it
parser = CommandParser(path)  # Create the parser, which initializes using data located in the path directory
source = CommandSource(callback)  # Create a source, which is used to talk back to the caller

input_ = 'command-that-returns-wow arg1 arg2'  # this command would call zzz__command-that-returns-wow.py with arg1 and arg2

parser.parse(CommandContext(input_, source))  # Parse the new context and run the command and callback (If no errors occur)
assert output == 'wow'

Command metadata:

Metadata for commands are stored in the commands.json file inside the commands_path of the parser. This is where all the data for the parser is loaded or stored.

All commands.json files are validated with JSON Schemas through the jsonschema python package

commands.json Draft-07 JSON Schema

key type description default
commandPrefix (Required) string Strings must start with this prefix, otherwise it is ignored. Empty string accepts all. N/A
commands (Required) array[Command] Contains metadata for the stored command modules. N/A

Command object Draft-07 JSON Schema

key type description default
name (Required) string Uniquely identifies the command to the CommandParser. N/A
usage string Usage information (How to use args). ""
description string Description of command. ""
permission integer The permission level the CommandSource requires to run the command. 0
function boolean, null Whether there is an associated python module to load. null
children array[Command] Sub-commands; these are handled by the parent's function. (No associated modules for themselves). []
overridable boolean Whether the CommandParser can override any data inside this object (must be manually enabled). true
disabled boolean If true still load command, but raise a DisabledError when attempting to execute. false

NOTE: Commands modules are not loaded unless they are listed in commands.json with the "function" key set to true.

Example commands.json contents:

{
  "commandPrefix": "!",
  "commands": [
    {
      "name": "test",
      "usage": "test [*args:any]",
      "description": "Test command.",
      "permission": 500,
      "function": true
    },
    {
      "name": "test2",
      "function": false
    }
  ]
}

Command modules:

Dynamically-loaded commands are denoted by filename with a prefix of "zzz__". Inside a command module, there is a function defined as "command". This function will be mapped to a Command's function attribute and stored in memory for execution. The function has access to any args that were parsed, as well as kwargs:

  1. 'self' (Command), which houses the metadata for the command that's being executed.

  2. 'parser' (CommandParser), which stores the list of registered commands and command data.

  3. 'context' (CommandContext), which supplies the CommandSource and the original text sent for parsing.

  • Any custom kwargs passed to CommandParser.parse.

Since commands cannot import their own modules, some are included in globals (math, random, and string).

Example command module:

def command(*args, **kwargs):
    self, context = kwargs.pop('self'), kwargs.pop('context')
    source = context.source
    if len(args) == 2:
        amount, sides = abs(int(getitem(args, 0))), abs(int(getitem(args, 1)))
        if amount > 0 and sides > 0:
            dice_rolls = [f"{(str(i + 1) + ':') if amount > 1 else ''} {str(random.randint(1, sides))}/{sides}" for i in range(amount)]
            source.send_feedback(f"/me \U0001f3b2 {source.display_name} rolled {'a die' if amount == 1 else str(amount) + ' dice'} with {sides} side{'' if sides == 1 else 's'}: {', '.join(dice_rolls)} \U0001f3b2")
        else:
            raise ImproperUsageError(self, context)
    else:
        raise ImproperUsageError(self, context)

At any time, you can call CommandParser.reload() to reload all command modules and metadata from disk storage.

Example file structure:

../
│
├───[commands_path]/
│       ├─── commands.json
│       ├─── zzz__[command1].py
│       ├─── zzz__[command2].py
│       └─── zzz__[command3].py
│

Adding/Removing Commands:

To add commands, you can either manually enter data into a commands.json file, or use the CommandParser.add_command(text: str, link: bool = False, **kwargs) method. The easiest way to use this method is to read the command module as text and pass that to the first argument. You can also store command modules online to allow for remote installation, as setting the link parameter to True will read text as a link, and will get the raw text data from that link. Ex: gist and pastebin.

NOTE: When adding a command, metadata for 'name' must to be filled. This can be done in the form of comments.

Example of metadata as comments:

# Name: points
# Usage: points [get (username:string)| set (username:string amount:integer)]
# Description: Get your current points
# Permission: 0
# Children: [{'name': 'get', 'usage': 'get (username:string)', 'permission':0}, {'name': 'set', 'usage': 'set (username:string amount:integer)', 'permission':500}]
def command(*args, **kwargs):
    ...

Removing an already added command is relatively easy. Just call CommandParser.remove_command(name: str) with the name of the command that you want to remove, and it will delete both the metadata and the command module from the disk.

If you don't want to delete the command when removing, a better alternative is to disable it with CommandParser.set_disabled(name: str, value: bool).

Metadata

Release files for dyncommands 1.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for dyncommands 1.3
File Size Uploaded
dyncommands-1.3.tar.gz 21.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for dyncommands 1.3
File Interpreter ABI Platform
dyncommands-1.3-py3-none-any.whl Python 3 none any Details

Total release size: 44.0 kB

Release files / dyncommands-1.3.tar.gz

Download URL dyncommands-1.3.tar.gz
Size 21.9 kB
Tags Source
SHA-256 checksum
How to use checksums
51098202dc444e4defeb49beba7cca9ab25186a8d2bc7664c0a437dec5e29c07
BLAKE2b-256 checksum
How to use checksums
026f2b41b35bc826cdca68cd8172007509d0eec0ac48fbdde88d67ce46a0cb9f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/3.7.1 importlib_metadata/4.10.0 pkginfo/1.8.2 requests/2.26.0 requests-toolbelt/0.9.1 tqdm/4.62.3 CPython/3.9.9

Release files / dyncommands-1.3-py3-none-any.whl

Download URL dyncommands-1.3-py3-none-any.whl
Size 22.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b4080afcacafde270f77183a613a5906aa44a1ea5291209186a47df3754849c0
BLAKE2b-256 checksum
How to use checksums
411f24f111b57fb276b01264ba6017d8a6b88bcc7dfac3c3f32005d18695546f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/3.7.1 importlib_metadata/4.10.0 pkginfo/1.8.2 requests/2.26.0 requests-toolbelt/0.9.1 tqdm/4.62.3 CPython/3.9.9

Release history Release notifications | RSS feed

This release

1.3 This release

2 release files

1.2

2 release files

1.1.2

2 release files

1.1.1

2 release files

1.1

2 release files

1.0.1

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page