diff --git a/README.md b/README.md index 6572c47636..c7362a4de5 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ The CLI tool contains all the Ape commands and the Python SDK contains the class ## **Ape Modular Plugin System:** -Our [list of plugins](https://www.apeworx.io/#plugins) is the best way to have the most interoperable experience with Web3. +Our modular plugin system is the best way to have the most interoperable experience with Web3. **NOTE**: If a plugin does not originate from the [ApeWorX GitHub Organization](https://github.com/ApeWorX?q=ape&type=all), you will get a warning about installing 3rd-party plugins. diff --git a/docs/userguides/clis.md b/docs/userguides/clis.md index 309703a90c..9138a19cce 100644 --- a/docs/userguides/clis.md +++ b/docs/userguides/clis.md @@ -18,7 +18,7 @@ This guide is for showcasing utilities that ship with Ape to assist in your CLI ## Ape Context Decorator -The `@ape_cli_context` gives you access to all the root Ape objects (`accounts`, `networks` etc.), the ape logger, and an `abort` method for stopping execution of your CLI gracefully. +The [@ape_cli_context](../methoddocs/cli.html#ape.cli.options.ape_cli_context) gives you access to all the root Ape objects (`accounts`, `networks` etc.), the ape logger, and an [abort](../methoddocs/cli.html#ape.cli.options.ApeCliContextObject.abort) method for stopping execution of your CLI gracefully. Here is an example using all of those features from the `cli_ctx`: ```python @@ -34,10 +34,37 @@ def cmd(cli_ctx): cli_ctx.abort(f"Bad account: {account.address}") ``` +In Ape, it is easy to extend the CLI context object and use the extended version in your CLIs: + +```python +from ape.cli import ApeCliContextObject, ape_cli_context +import click + +class MyManager: + """My custom manager.""" + +class CustomContext(ApeCliContextObject): + """Add new managers to your custom context""" + my_manager: MyManager = MyManager() + + @property + def signer(self): + """Utilize existing managers in your custom context.""" + return self.account_manager.load("my_account") + +@click.command() +@ape_cli_context(obj_type=CustomContext) +def cli(cli_ctx): + # Access your manager. + print(cli_ctx.my_manager) + # Access other Ape managers. + print(cli_ctx.account_manager) +``` + ## Network Tools -The `@network_option()` allows you to select an ecosystem / network / provider combination. -When using with the `NetworkBoundCommand` cls, you can cause your CLI to connect before any of your code executes. +The [@network_option()](../methoddocs/cli.html#ape.cli.options.network_option) allows you to select an ecosystem / network / provider combination. +When using with the [NetworkBoundCommand](../methoddocs/cli.html#ape.cli.commands.NetworkBoundCommand) class, you can cause your CLI to connect before any of your code executes. This is useful if your script or command requires a provider connection in order for it to run. ```python @@ -62,7 +89,7 @@ def cmd(network): ## Account Tools -Use the `@account_option()` for adding an option to your CLIs to select an account. +Use the [@account_option()](../methoddocs/cli.html#ape.cli.options.account_option) for adding an option to your CLIs to select an account. This option does several things: 1. If you only have a single account in Ape (from both test accounts _and_ other accounts), it will use that account as the default. @@ -96,7 +123,7 @@ And when invoking the command from the CLI, it would look like the following: cmd --account TEST::0 # Use the test account at index 0. ``` -Alternatively, you can call the `get_user_selected_account()` directly to have more control of when the account gets selected: +Alternatively, you can call the [get_user_selected_account()](../methoddocs/cli.html#ape.cli.choices.get_user_selected_account) directly to have more control of when the account gets selected: ```python import click @@ -110,8 +137,8 @@ def cmd(): ``` Similarly, there are a couple custom arguments for aliases alone that are useful when making CLIs for account creation. -If you use `@existing_alias_argument()` and specify an alias does not already exist, it will error. -And visa-versa when using `@non_existing_alias_argument()` +If you use [@existing_alias_argument()](../methoddocs/cli.html#ape.cli.arguments.existing_alias_argument) and specify an alias does not already exist, it will error. +And visa-versa when using [@non_existing_alias_argument()](../methoddocs/cli.html#ape.cli.arguments.non_existing_alias_argument). ```python import click diff --git a/docs/userguides/networks.md b/docs/userguides/networks.md index 4ea863dbf2..6e5b949834 100644 --- a/docs/userguides/networks.md +++ b/docs/userguides/networks.md @@ -135,3 +135,94 @@ from ape import chain block = chain.provider.get_block("latest") ``` + +## Provider Context Manager + +Use the [ProviderContextManager](../methoddocs/api.html#ape.api.networks.ProviderContextManager) to change the network-context in Python. +When entering a network for the first time, it will connect to that network. +**You do not need to call `.connect()` or `.disconnect()` manually**. + +For example, if you are using a script with a default network connection, you can change connection in the middle of the script by using the provider context manager: + +```python +from ape import chain, networks + +def main(): + start_provider = chain.provider.name + with networks.ethereum.mainnet.use_provider("geth") as provider: + # We are using a different provider than the one we started with. + assert start_provider != provider.name +``` + +Jump between networks to simulate multi-chain behavior. + +```python +import click +from ape import networks + +@click.command() +def cli(): + with networks.polygon.mainnet.use_provider("geth"): + ... + with networks.ethereum.mainnet.use_provider("geth"): + ... +``` + +The argument to [use_provider()](../methoddocs/api.html#ape.api.networks.NetworkAPI.use_provider) is the name of the provider you want to use. +You can also tell Ape to use the default provider by calling method [use_default_provider()](../methoddocs/api.html#ape.api.networks.NetworkAPI.use_default_provider) instead. +This will use whatever provider is set as default for your ecosystem / network combination (via one of your `ape-config.yaml` files). + +For example, let's say I have a default provider set like this: + +```yaml +arbitrum: + mainnet: + default_provider: alchemy +``` + +```python +import ape + +# Use the provider configured as the default for the arbitrum::mainnet network. +# In this case, it will use the "alchemy" provider. +with ape.networks.arbitrum.mainnet.use_default_provider(): + ... +``` + +You can also use the [parse_network_choice()](../methoddocs/managers.html#ape.managers.networks.NetworkManager.parse_network_choice) method when working with network choice strings: + +```python +from ape import networks + +# Same as doing `networks.ethereum.local.use_provider("test")`. +with networks.parse_network_choice("ethereum:local:test") as provider: + print(provider) +``` + +**A note about disconnect**: Providers do not disconnect until the very end of your Python session. +This is so you can easily switch network contexts in a bridge or multi-chain environment, which happens in fixtures and other sessions out of Ape's control. +However, sometimes you may definitely want your temporary network session to end before continuing, in which case you can use the `disconnect_after=True` kwarg: + +```python +from ape import networks + +with networks.parse_network_choice("ethereum:local:foundry", disconnect_after=True) as provider: + print(provider) +``` + +### Forked Context + +Using the `networks.fork()` method, you can achieve similar effects to using a forked network with `disconnect_after=True`. +For example, let's say we are running the following script on the network `ethereum:mainnet`. +We can switch to a forked network by doing this: + +```python +from ape import networks + +def main(): + with networks.fork("foundry"): + ... + # Do stuff on a local, forked version of mainnet + + # Switch back to mainnet. +``` diff --git a/docs/userguides/testing.md b/docs/userguides/testing.md index 7031645262..b20f0ce79b 100644 --- a/docs/userguides/testing.md +++ b/docs/userguides/testing.md @@ -47,17 +47,12 @@ To disable isolation add the `--disable-isolation` flag when running `ape test` ## Fixtures -Fixtures are any type of reusable instances of something with configurable scopes. `pytest` handles passing fixtures -into each test method as test-time. To learn more about [fixtures](https://docs.pytest.org/en/7.1.x/explanation/fixtures.html) +You can define and use `pytest` fixtures in your Ape tests. +Learn more about fixtures from [this guide](https://docs.pytest.org/en/7.1.x/explanation/fixtures.html). +The syntax and functionalities of fixtures work exactly the same in Ape as it does with `pytest`. -Define fixtures for static data used by tests. This data can be accessed by all tests in the suite unless specified otherwise. This could be data as well as helpers of modules which will be passed to all tests. - -A common place to define fixtures are in the **conftest.py** which should be saved under the test directory: - -conftest.py is used to import external plugins or modules. By defining the following global variable, pytest will load the module and make it available for its test. - -You can define your own fixtures or use existing ones. The `ape-test` plugin comes -with fixtures you will likely want to use: +The `ape-test` plugin comes with fixtures you will likely want to use. +The following guide explains each fixture that comes with Ape. ### accounts fixture diff --git a/src/ape/api/networks.py b/src/ape/api/networks.py index a0a6fbe2b1..066fd9b452 100644 --- a/src/ape/api/networks.py +++ b/src/ape/api/networks.py @@ -566,6 +566,11 @@ class ProviderContextManager(ManagerAccessMixin): mainnet = networks.ethereum.mainnet # An instance of NetworkAPI with mainnet.use_provider("infura"): ... + + # Or, using choice-strings: + + with networks.parse_network_choice("ethereum:local:test"): + ... """ connected_providers: Dict[str, "ProviderAPI"] = {} diff --git a/src/ape/cli/__init__.py b/src/ape/cli/__init__.py index 6e5ecbab01..8204b3fcf7 100644 --- a/src/ape/cli/__init__.py +++ b/src/ape/cli/__init__.py @@ -14,6 +14,7 @@ ) from ape.cli.commands import NetworkBoundCommand from ape.cli.options import ( + ApeCliContextObject, account_option, ape_cli_context, contract_option, @@ -33,6 +34,7 @@ "Alias", "AllFilePaths", "ape_cli_context", + "ApeCliContextObject", "contract_file_paths_argument", "contract_option", "existing_alias_argument", diff --git a/src/ape/cli/options.py b/src/ape/cli/options.py index 1341c5b81b..0cf5efb0bf 100644 --- a/src/ape/cli/options.py +++ b/src/ape/cli/options.py @@ -1,4 +1,4 @@ -from typing import Callable, Dict, List, NoReturn, Optional, Union +from typing import Callable, Dict, List, NoReturn, Optional, Type, Union import click from ethpm_types import ContractType @@ -75,7 +75,9 @@ def set_level(ctx, param, value): } -def ape_cli_context(default_log_level: str = DEFAULT_LOG_LEVEL): +def ape_cli_context( + default_log_level: str = DEFAULT_LOG_LEVEL, obj_type: Type = ApeCliContextObject +): """ A ``click`` context object with helpful utilities. Use in your commands to get access to common utility features, @@ -84,11 +86,16 @@ def ape_cli_context(default_log_level: str = DEFAULT_LOG_LEVEL): Args: default_log_level (str): The log-level value to pass to :meth:`~ape.cli.options.verbosity_option`. + obj_type (Type): The context object type. Defaults to + :class:`~ape.cli.options.ApeCliContextObject`. Sub-class + the context to extend its functionality in your CLIs, + such as if you want to add additional manager classes + to the context. """ def decorator(f): f = verbosity_option(logger, default=default_log_level)(f) - f = click.make_pass_decorator(ApeCliContextObject, ensure=True)(f) + f = click.make_pass_decorator(obj_type, ensure=True)(f) return f return decorator diff --git a/src/ape/types/signatures.py b/src/ape/types/signatures.py index fdd182aa28..0e8ec3611b 100644 --- a/src/ape/types/signatures.py +++ b/src/ape/types/signatures.py @@ -7,6 +7,11 @@ from ape.types import AddressType +# Fix 404 in doc link. +SignableMessage.__doc__ = (SignableMessage.__doc__ or "").replace( + "EIP-191_", "`EIP-191 `__" +) + def _left_pad_bytes(val: bytes, num_bytes: int) -> bytes: return b"\x00" * (num_bytes - len(val)) + val if len(val) < num_bytes else val