Developing Plugins
Your plugin project can be any type of python project, so long as its package name starts with ape-
(such as ape-ethereum
).
The module and plugin directory name must start with ape_
(such as ape_ethereum
).
To create an ape
plugin, implement one or more API classes from the ape.api
namespace and/or add key
ape_cli_subcommands
to your entry-points list in your project’s setup.py
, depending on what type of plugin you want to create.
This guide is intended to assist in both of those use cases.
The following is a list of example plugins to use as a reference when developing plugins:
the Solidity plugin, an example
CompilerAPI
the Infura plugin, an example
ProviderAPI
the Trezor plugin, an example
AccountAPI
the Tokenlists plugin, an example CLI Extension
Initialize a Plugin Project
As previously mentioned, a plugin project is merely a python project. However, you can optionally use this project template for initializing your plugin.
Note
This template is primarily designed for plugins built within the ApeWorX team organization; not everything may apply.
It is okay to delete anything that does not work or that you don’t find helpful. The template may be good to follow if you want to keep your plugin of similar quality to plugins developed by the ApeWorX team.
Implementing API Classes
API classes (classes from the ape.api
namespace) are primarily composed of abstract methods and properties that plugins must implement.
A benefit of the plugin system is that each plugin can implement these however they need, so long as they conform to the API interface.
Two plugins with the same API may do entirely different things and yet be interchangeable in their usage.
To implement an API, import its class and use it as a base-class in your implementation class.
Warning
The plugin will fail to work properly if you do not implement all the abstract methods.
from ape.api import ProviderAPI
from web3 import Web3, HTTPProvider
class MyProvider(ProviderAPI):
_web3: Web3 = None # type: ignore
def connect(self):
self._web3 = Web3(HTTPProvider(str("https://localhost:1337")))
"""Implement rest of abstract methods"""
Registering API Classes
Once you have finished implementing your API classes, you need to register them using the @plugins.register method decorator.
from ape import plugins
# Here, we register our provider plugin so we can use it in 'ape'.
@plugins.register(plugins.ProviderPlugin)
def providers():
# NOTE: By keeping this import local, we avoid slower plugin load times.
from ape_my_plugin.provider import MyProvider
# NOTE: 'MyProvider' defined in a prior code-block.
yield "ethereum", "local", MyProvider
This decorator hooks into ape core and ties everything together by looking for all local installed site-packages that start with ape_
.
Then, it will loop through these potential ape
plugins and see which ones have created a plugin type registration.
If the plugin type registration is found, then ape
knows this package is a plugin and attempts to process it according to its registration interface.
Warning
Ensure your plugin’s __init__.py
file imports quickly by keeping all expensive imports in the hook functions locally.
This helps Ape register plugins faster, which is required when checking for API implementations.
CLI Plugins
The ape
CLI is built using the python package click.
To create a CLI plugin, create any type of click
command (such as a click.group
or a click.command
).
_cli.py
:
import click
@click.group
def cli():
"""My custom commands."""
@cli.command()
def my_sub_cmd():
"""My subcommand."""
Then, register it using entrypoints
, which is a built-in python registry of items declared in setup.py
.
setup.py
:
...
entry_points={
"ape_cli_subcommands": [
"ape_myplugin=ape_myplugin._cli:cli",
],
},
...
Note
Typically, a _cli.py
module is used instead of a __init__.py
module for the location of the Click CLI group because it is logically separate from the Python module loading process.
If you try to define them together and use ape
as a library as well, there is a race condition in the loading process that will prevent the CLI plugin from working.
For common click
usages, use the ape.cli
namespace.
For example, use the @existing_alias_argument() decorator) when you need a CLI argument for specifying an existing account alias:
Follow this guide to learn more about what you can do with the utilities found in ape.cli
.
import click
from ape.cli import existing_alias_argument
@click.command()
@existing_alias_argument()
def my_cmd(alias):
click.echo(f"{alias} is an existing account!")
Using Plugins
Once you have finished implementing and registering your API classes, they will now be part of ape
. For example,
if you implemented the AccountAPI
, you can now use accounts created from this plugin. The top-level ape
manager
classes are indifferent about the source of the plugin.
from ape import accounts
# The manager can load accounts from any account-based plugin.
my_ledger_account = accounts.load("ledger_0") # Created using the 'ape-ledger' plugin
my_trezor_account = accounts.load("trezor_0") # Created using the 'ape-trezor' plugin
Similarly, if you implemented a ProviderAPI
, that provider is now accessible in the CLI via the --network
option:
ape console my_script --network ethereum:local:my_provider_plugin
Note
The --network
option is available on the commands test
and console
as well as any CLI command that uses the network option decorator.
To learn more about networks in Ape, follow this guide.
When creating the CLI-based plugins, you should see your CLI command as a top-level command in the ape --help
output:
Commands:
...
my-plugin Utilities for my plugin
...
To edit the description of the CLI command (or group), you can either set the short_help
kwarg or use a doc-str on the command:
import click
@click.command(short_help="Utilities for my plugin")
def cli():
pass
""" Or """
@click.command()
def cli():
"""Utilities for my plugin"""
Logging
Use Ape’s logger in your plugin by importing it from the ape.logging
module or by using it off the CLI context (from using the @ape_cli_context
decorator).
Import the logger from the logging module
from ape.logging import logger
logger.info("This is a log message")
Use the logger from the @ape_cli_context
from ape.cli import ape_cli_context
@ape_cli_context()
def my_command(cli_ctx):
cli_ctx.logger.info("my log message")