Skip to content

Commit

Permalink
Merge branch 'main' into fix/commitlint
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey authored Jan 28, 2025
2 parents a89022e + 978c296 commit d881933
Show file tree
Hide file tree
Showing 23 changed files with 332 additions and 56 deletions.
51 changes: 50 additions & 1 deletion docs/userguides/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,58 @@ plugin:
This helps keep your secrets out of Ape!
Similarly, any config key-name can also be set with the same named environment variable (with a prefix).
If a configuration is left unset (i.e., not included in the `ape-config.(yaml|json|toml)` file, Ape will inspect the environment variables as a fallback, following the pattern `APE_<PLUGIN?>_SETTING`, where different plugins define different prefixes.

For example, the following config:

```yaml
contracts_folder: src/qwe
test:
number_of_accounts: 3
show_internal: True
compile:
exclude:
- "one"
- "two"
- "three"
include_dependencies: true
```

could be entirely defined with environment variables as follows:

```shell
APE_CONTRACTS_FOLDER=src/contracts
APE_TEST_NUMBER_OF_ACCOUNTS=3
APE_TEST_SHOW_INTERNAL=true
APE_COMPILE_EXCLUDE='["one", "two", "three"]'
APE_COMPILE_INCLUDE_DEPENDENCIES=true
```

Notice the `ape-compile` and `ape-test` plugin include their plugin name `APE_COMPILE` and `APE_TEST` respectively where `contracts_folder` only has the prefix `APE_` since it is not part of a plugin.

Here is the complete list of supported prefixes that come with Ape out-of-the-box:

| Module/Plugin | Prefix |
| ------------- | ------------ |
| ape | APE |
| ape_cache | APE_CACHE |
| ape_compile | APE_COMPILE |
| ape_console | APE_CONSOLE |
| ape_ethereum | APE_ETHEREUM |
| ape_networks | APE_NETWORKS |
| ape_node | APE_NODE |
| ape_test | APE_TEST |

Each plugin outside the core package may define its own prefix, but the standard is `APE_PLUGINNAME_`.

Using environment variables assists in keeping secrets out of your config files.
However, the primary config should be file-driven and environment variables should only be used when necessary.

## Base Path

Change the base path if it is different than your project root.
Change the base path if it is different from your project root.
For example, imagine a project structure like:

```
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ norecursedirs = "projects"
# And 'pytest_ethereum' is not used and causes issues in some environments.
addopts = """
-p no:pytest_ethereum
-p no:boa_test
"""

python_files = "test_*.py"
Expand Down
5 changes: 2 additions & 3 deletions src/ape/api/address.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,8 @@ def code(self) -> "ContractCode":
"""
The raw bytes of the smart-contract code at the address.
"""

# TODO: Explore caching this (based on `self.provider.network` and examining code)
return self.provider.get_code(self.address)
# NOTE: Chain manager handles code caching.
return self.chain_manager.get_code(self.address)

@property
def codesize(self) -> int:
Expand Down
6 changes: 3 additions & 3 deletions src/ape/api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class PluginConfig(BaseSettings):
a config API must register a subclass of this class.
"""

model_config = SettingsConfigDict(extra="allow")
model_config = SettingsConfigDict(extra="allow", env_prefix="APE_")

@classmethod
def from_overrides(
Expand Down Expand Up @@ -285,7 +285,7 @@ class ApeConfig(ExtraAttributesMixin, BaseSettings, ManagerAccessMixin):

def __init__(self, *args, **kwargs):
project_path = kwargs.get("project")
super(BaseSettings, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
# NOTE: Cannot reference `self` at all until after super init.
self._project_path = project_path

Expand Down Expand Up @@ -350,7 +350,7 @@ def __init__(self, *args, **kwargs):
"""

# NOTE: Plugin configs are technically "extras".
model_config = SettingsConfigDict(extra="allow")
model_config = SettingsConfigDict(extra="allow", env_prefix="APE_")

@model_validator(mode="before")
@classmethod
Expand Down
86 changes: 61 additions & 25 deletions src/ape/managers/_contractscache.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,13 +270,22 @@ def _delete_proxy(self, address: AddressType):
def __contains__(self, address: AddressType) -> bool:
return self.get(address) is not None

def cache_deployment(self, contract_instance: ContractInstance):
def cache_deployment(
self,
contract_instance: ContractInstance,
proxy_info: Optional[ProxyInfoAPI] = None,
detect_proxy: bool = True,
):
"""
Cache the given contract instance's type and deployment information.
Args:
contract_instance (:class:`~ape.contracts.base.ContractInstance`): The contract
to cache.
proxy_info (Optional[ProxyInfoAPI]): Pass in the proxy info, if it is known, to
avoid the potentially expensive look-up.
detect_proxy (bool): Set to ``False`` to avoid detecting if the contract is a
proxy.
"""
address = contract_instance.address
contract_type = contract_instance.contract_type # may be a proxy
Expand All @@ -285,24 +294,22 @@ def cache_deployment(self, contract_instance: ContractInstance):
# in case it is needed somewhere. It may get overridden.
self.contract_types.memory[address] = contract_type

if proxy_info := self.provider.network.ecosystem.get_proxy_info(address):
# The user is caching a deployment of a proxy with the target already set.
self.cache_proxy_info(address, proxy_info)
if implementation_contract := self.get(proxy_info.target):
updated_proxy_contract = _get_combined_contract_type(
contract_type, proxy_info, implementation_contract
)
self.contract_types[address] = updated_proxy_contract
if proxy_info:
# Was given proxy info.
self._cache_proxy_contract(address, proxy_info, contract_type, contract_instance)

# Use this contract type in the user's contract instance.
contract_instance.contract_type = updated_proxy_contract
elif detect_proxy:
# Proxy info was not provided. Use the connected ecosystem to figure it out.
if proxy_info := self.provider.network.ecosystem.get_proxy_info(address):
# The user is caching a deployment of a proxy with the target already set.
self._cache_proxy_contract(address, proxy_info, contract_type, contract_instance)

else:
# No implementation yet. Just cache proxy.
# Cache as normal.
self.contract_types[address] = contract_type

else:
# Regular contract. Cache normally.
# Cache as normal; do not do expensive proxy detection.
self.contract_types[address] = contract_type

# Cache the deployment now.
Expand All @@ -312,6 +319,26 @@ def cache_deployment(self, contract_instance: ContractInstance):

return contract_type

def _cache_proxy_contract(
self,
address: AddressType,
proxy_info: ProxyInfoAPI,
contract_type: ContractType,
contract_instance: ContractInstance,
):
self.cache_proxy_info(address, proxy_info)
if implementation_contract := self.get(proxy_info.target):
updated_proxy_contract = _get_combined_contract_type(
contract_type, proxy_info, implementation_contract
)
self.contract_types[address] = updated_proxy_contract

# Use this contract type in the user's contract instance.
contract_instance.contract_type = updated_proxy_contract
else:
# No implementation yet. Just cache proxy.
self.contract_types[address] = contract_type

def cache_proxy_info(self, address: AddressType, proxy_info: ProxyInfoAPI):
"""
Cache proxy info for a particular address, useful for plugins adding already
Expand Down Expand Up @@ -492,6 +519,8 @@ def get(
address: AddressType,
default: Optional[ContractType] = None,
fetch_from_explorer: bool = True,
proxy_info: Optional[ProxyInfoAPI] = None,
detect_proxy: bool = True,
) -> Optional[ContractType]:
"""
Get a contract type by address.
Expand All @@ -506,6 +535,9 @@ def get(
fetch_from_explorer (bool): Set to ``False`` to avoid fetching from an
explorer. Defaults to ``True``. Only fetches if it needs to (uses disk
& memory caching otherwise).
proxy_info (Optional[ProxyInfoAPI]): Pass in the proxy info, if it is known,
to avoid the potentially expensive look-up.
detect_proxy (bool): Set to ``False`` to avoid detecting if it is a proxy.
Returns:
Optional[ContractType]: The contract type if it was able to get one,
Expand All @@ -531,13 +563,14 @@ def get(

else:
# Contract is not cached yet. Check broader sources, such as an explorer.
# First, detect if this is a proxy.
if not (proxy_info := self.proxy_infos[address_key]):
if proxy_info := self.provider.network.ecosystem.get_proxy_info(address_key):
self.proxy_infos[address_key] = proxy_info
if not proxy_info and detect_proxy:
# Proxy info not provided. Attempt to detect.
if not (proxy_info := self.proxy_infos[address_key]):
if proxy_info := self.provider.network.ecosystem.get_proxy_info(address_key):
self.proxy_infos[address_key] = proxy_info

if proxy_info:
# Contract is a proxy.
# Contract is a proxy (either was detected or provided).
implementation_contract_type = self.get(proxy_info.target, default=default)
proxy_contract_type = (
self._get_contract_type_from_explorer(address_key)
Expand All @@ -554,12 +587,6 @@ def get(
self.contract_types[address_key] = contract_type_to_cache
return contract_type_to_cache

if not self.provider.get_code(address_key):
if default:
self.contract_types[address_key] = default

return default

# Also gets cached to disk for faster lookup next time.
if fetch_from_explorer:
contract_type = self._get_contract_type_from_explorer(address_key)
Expand Down Expand Up @@ -594,6 +621,8 @@ def instance_at(
txn_hash: Optional[Union[str, "HexBytes"]] = None,
abi: Optional[Union[list[ABI], dict, str, Path]] = None,
fetch_from_explorer: bool = True,
proxy_info: Optional[ProxyInfoAPI] = None,
detect_proxy: bool = True,
) -> ContractInstance:
"""
Get a contract at the given address. If the contract type of the contract is known,
Expand All @@ -618,6 +647,9 @@ def instance_at(
fetch_from_explorer (bool): Set to ``False`` to avoid fetching from the explorer.
Defaults to ``True``. Won't fetch unless it needs to (uses disk & memory caching
first).
proxy_info (Optional[ProxyInfoAPI]): Pass in the proxy info, if it is known, to avoid
the potentially expensive look-up.
detect_proxy (bool): Set to ``False`` to avoid detecting if the contract is a proxy.
Returns:
:class:`~ape.contracts.base.ContractInstance`
Expand All @@ -640,7 +672,11 @@ def instance_at(
try:
# Always attempt to get an existing contract type to update caches
contract_type = self.get(
contract_address, default=contract_type, fetch_from_explorer=fetch_from_explorer
contract_address,
default=contract_type,
fetch_from_explorer=fetch_from_explorer,
proxy_info=proxy_info,
detect_proxy=detect_proxy,
)
except Exception as err:
if contract_type or abi:
Expand Down
29 changes: 26 additions & 3 deletions src/ape/managers/chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@
if TYPE_CHECKING:
from rich.console import Console as RichConsole

from ape.types.trace import GasReport, SourceTraceback
from ape.types.vm import SnapshotID
from ape.types import BlockID, ContractCode, GasReport, SnapshotID, SourceTraceback


class BlockContainer(BaseManager):
Expand Down Expand Up @@ -703,7 +702,7 @@ def _get_console(self, *args, **kwargs):
class ChainManager(BaseManager):
"""
A class for managing the state of the active blockchain.
Also handy for querying data about the chain and managing local caches.
Also, handy for querying data about the chain and managing local caches.
Access the chain manager singleton from the root ``ape`` namespace.
Usage example::
Expand All @@ -716,6 +715,7 @@ class ChainManager(BaseManager):
_block_container_map: dict[int, BlockContainer] = {}
_transaction_history_map: dict[int, TransactionHistory] = {}
_reports: ReportManager = ReportManager()
_code: dict[str, dict[str, dict[AddressType, "ContractCode"]]] = {}

@cached_property
def contracts(self) -> ContractCache:
Expand Down Expand Up @@ -965,3 +965,26 @@ def get_receipt(self, transaction_hash: str) -> ReceiptAPI:
raise TransactionNotFoundError(transaction_hash=transaction_hash)

return receipt

def get_code(
self, address: AddressType, block_id: Optional["BlockID"] = None
) -> "ContractCode":
network = self.provider.network

# Two reasons to avoid caching:
# 1. dev networks - chain isolation makes this mess up
# 2. specifying block_id= kwarg - likely checking if code
# exists at the time and shouldn't use cache.
skip_cache = network.is_dev or block_id is not None
if skip_cache:
return self.provider.get_code(address, block_id=block_id)

self._code.setdefault(network.ecosystem.name, {})
self._code[network.ecosystem.name].setdefault(network.name, {})
if address in self._code[network.ecosystem.name][network.name]:
return self._code[network.ecosystem.name][network.name][address]

# Get from RPC for the first time AND use cache.
code = self.provider.get_code(address)
self._code[network.ecosystem.name][network.name][address] = code
return code
3 changes: 3 additions & 0 deletions src/ape_cache/config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from pydantic_settings import SettingsConfigDict

from ape.api.config import PluginConfig


class CacheConfig(PluginConfig):
size: int = 1024**3 # 1gb
model_config = SettingsConfigDict(extra="allow", env_prefix="APE_CACHE_")
3 changes: 3 additions & 0 deletions src/ape_compile/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import Union

from pydantic import field_serializer, field_validator
from pydantic_settings import SettingsConfigDict

from ape.api.config import ConfigEnum, PluginConfig
from ape.utils.misc import SOURCE_EXCLUDE_PATTERNS
Expand Down Expand Up @@ -53,6 +54,8 @@ class Config(PluginConfig):
Extra selections to output. Outputs to ``.build/{key.lower()}``.
"""

model_config = SettingsConfigDict(extra="allow", env_prefix="APE_COMPILE_")

@field_validator("exclude", mode="before")
@classmethod
def validate_exclude(cls, value):
Expand Down
4 changes: 4 additions & 0 deletions src/ape_console/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from pydantic_settings import SettingsConfigDict

from ape.api.config import PluginConfig


class ConsoleConfig(PluginConfig):
plugins: list[str] = []
"""Additional IPython plugins to include in your session."""

model_config = SettingsConfigDict(extra="allow", env_prefix="APE_CONSOLE_")
Loading

0 comments on commit d881933

Please sign in to comment.