Skip to content

Commit

Permalink
feat: support pyproject.toml Ape configuration (#2326)
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey authored Oct 21, 2024
1 parent 8456fa8 commit ca8786a
Show file tree
Hide file tree
Showing 11 changed files with 258 additions and 40 deletions.
83 changes: 80 additions & 3 deletions docs/userguides/config.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Configure Ape

You can configure Ape using configuration files with the name `ape-config.yaml`.
There are two locations you can place an `ape-config.yaml` file.
You can configure Ape using a `pyproject.toml` file and the prefix `tool.ape` or any configuration file named `ape-config.[yaml|yml|json]`.
There are two locations you can place config files.

1. In the root of your project
2. In your `$HOME/.ape` directory (global)
Expand All @@ -23,6 +23,13 @@ However, here is a list of common-use cases requiring the `ape-config.yaml` file
**Environment Variables**: `ape-config.yaml` files support environment-variable expansion.
Simply include environment variables (with the `$` prefix) in your config file and Ape will automatically expand them.

```toml
[tool.ape.plugin]
secret_rpc = "$MY_SECRET_RPC"
```

Or the equivalent YAML:

```yaml
plugin:
secret_rpc: $MY_SECRET_RPC
Expand All @@ -44,6 +51,13 @@ project

In this case, you want to configure Ape like:

```toml
[tool.ape]
base_path = "src"
```

Or the equivalent YAML:

```yaml
base_path: src
```
Expand All @@ -56,6 +70,13 @@ Some dependencies, such as python-based ones like `snekmate`, use this structure
Specify a different path to your `contracts/` directory.
This is useful when using a different naming convention, such as `src/` rather than `contracts/`.

```toml
[tool.ape]
contracts_folder = "src"
```

Or the equivalent YAML:

```yaml
contracts_folder: src
```
Expand All @@ -71,6 +92,13 @@ contracts_folder: "~/GlobalContracts"

You can change the default ecosystem by including the following:

```toml
[tool.ape]
default_ecosystem = "fantom"
```

Or the equivalent YAML:

```yaml
default_ecosystem: fantom
```
Expand All @@ -84,9 +112,18 @@ To learn more about dependencies, see [this guide](./dependencies.html).

A simple example of configuring dependencies looks like this:

```toml
[[tool.ape.dependencies]]
name = "openzeppelin"
github = "OpenZeppelin/openzeppelin-contracts"
version = "4.4.2"
```

Or the equivalent YAML:

```yaml
dependencies:
- name: OpenZeppelin
- name: openzeppelin
github: OpenZeppelin/openzeppelin-contracts
version: 4.4.2
```
Expand All @@ -98,6 +135,18 @@ Set deployments that were made outside of Ape in your `ape-config.yaml` to creat

Config example:

```toml
[[tool.ape.deployments.ethereum.mainnet]]
contract_type = "MyContract"
address = "0x5FbDB2315678afecb367f032d93F642f64180aa3"
[[tool.ape.deployments.ethereum.sepolia]]
contract_type = "MyContract"
address = "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512"
```

Or the equivalent YAML:

```yaml
deployments:
ethereum:
Expand Down Expand Up @@ -126,6 +175,13 @@ Ape does not add or edit deployments in your `ape-config.yaml` file.
When using the `node` provider, you can customize its settings.
For example, to change the URI for an Ethereum network, do:

```toml
[tool.ape.node.ethereum.mainnet]
uri = "http://localhost:5030"
```

Or the equivalent YAML:

```yaml
node:
ethereum:
Expand All @@ -145,6 +201,16 @@ For more information on networking as a whole, see [this guide](./networks.html)

Set default network and network providers:

```toml
[tool.ape.ethereum]
default_network = "mainnet-fork"

[tool.ape.ethereum.mainnet_fork]
default_provider = "hardhat"
```

Or the equivalent YAML:

```yaml
ethereum:
default_network: mainnet-fork
Expand Down Expand Up @@ -188,6 +254,17 @@ Set which `ape` plugins you want to always use.
The `ape-` prefix is not needed and shouldn't be included here.
```

```toml
[[tool.ape.plugins]]
name = "solidity"
version = "0.1.0b2"

[[tool.ape.plugins]]
name = "ens"
```

Or the equivalent YAML:

```yaml
plugins:
- name: solidity # ape-solidity plugin
Expand Down
15 changes: 15 additions & 0 deletions src/ape/api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,21 @@ def __ape_extra_attributes__(self) -> Iterator[ExtraModelAttributes]:

@classmethod
def validate_file(cls, path: Path, **overrides) -> "ApeConfig":
"""
Create an ApeConfig class using the given path.
Supports both pyproject.toml and ape-config.[.yml|.yaml|.json] files.
Raises:
:class:`~ape.exceptions.ConfigError`: When given an unknown file type
or the data is invalid.
Args:
path (Path): The path to the file.
**overrides: Config overrides.
Returns:
:class:`~ape.api.config.ApeConfig`
"""
data = {**load_config(path), **overrides}

# NOTE: We are including the project path here to assist
Expand Down
14 changes: 13 additions & 1 deletion src/ape/api/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,25 @@ def is_valid(self) -> bool:

@cached_property
def config_file(self) -> Path:
if self._using_pyproject_toml:
return self._pyproject_toml

# else: check for an ape-config file.
for ext in self.EXTENSIONS:
path = self.path / f"{self.CONFIG_FILE_NAME}{ext}"
if path.is_file():
return path

# Default
# Default: non-existing ape-config.yaml file.
return self.path / f"{self.CONFIG_FILE_NAME}.yaml"

@property
def _pyproject_toml(self) -> Path:
return self.path / "pyproject.toml"

@property
def _using_pyproject_toml(self) -> bool:
return self._pyproject_toml.is_file() and "[tool.ape" in self._pyproject_toml.read_text()

def extract_config(self, **overrides) -> ApeConfig:
return ApeConfig.validate_file(self.config_file, **overrides)
2 changes: 2 additions & 0 deletions src/ape/managers/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -1763,6 +1763,8 @@ def unpack(self, destination: Path, config_override: Optional[dict] = None) -> "
path.write_text(str(src.content), encoding="utf8")

# Unpack config file.
# NOTE: Always unpacks into a regular .yaml config file for simplicity
# and maximum portibility.
self.config.write_to_disk(destination / "ape-config.yaml")

return LocalProject(destination, config_override=config_override)
Expand Down
12 changes: 11 additions & 1 deletion src/ape/utils/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
import inspect
import json
import sys

if sys.version_info.minor >= 11:
# 3.11 or greater
# NOTE: type-ignore is for when running mypy on python versions < 3.11
import tomllib # type: ignore[import-not-found]
else:
import toml as tomllib # type: ignore[no-redef]

from asyncio import gather
from collections.abc import Coroutine, Mapping
from datetime import datetime, timezone
Expand Down Expand Up @@ -208,7 +216,9 @@ def load_config(path: Path, expand_envars=True, must_exist=False) -> dict:
if expand_envars:
contents = expand_environment_variables(contents)

if path.suffix in (".json",):
if path.name == "pyproject.toml":
config = tomllib.loads(contents).get("tool", {}).get("ape", {})
elif path.suffix in (".json",):
config = json.loads(contents)
elif path.suffix in (".yml", ".yaml"):
config = yaml.safe_load(contents)
Expand Down
2 changes: 1 addition & 1 deletion src/ape_pm/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ def _handle_package_path(path: Path, original_value: Optional[str] = None) -> di
value = original_value or path.as_posix()
raise click.BadArgumentUsage(f"Unknown package '{value}'.")

elif path.is_file() and path.name == "ape-config.yaml":
elif path.is_file() and (path.stem == "ape-config" or path.name == "pyproject.toml"):
path = path.parent

path = path.resolve().absolute()
Expand Down
5 changes: 5 additions & 0 deletions src/ape_pm/dependency.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ def validate_local_path(cls, model):
# Resolves the relative path so if the dependency API
# data moves, it will still work.
path = Path(model["local"])

# Automatically include `"name"`.
if "name" not in model:
model["name"] = path.stem

if path.is_absolute():
return model

Expand Down
112 changes: 105 additions & 7 deletions tests/functional/test_config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import re
from pathlib import Path
from typing import Optional, Union

Expand All @@ -15,6 +16,90 @@
from ape_networks import CustomNetwork
from tests.functional.conftest import PROJECT_WITH_LONG_CONTRACTS_FOLDER

CONTRACTS_FOLDER = "pathsomewhwere"
NUMBER_OF_TEST_ACCOUNTS = 31
YAML_CONTENT = rf"""
contracts_folder: "{CONTRACTS_FOLDER}"
dependencies:
- name: "openzeppelin"
github: "OpenZeppelin/openzeppelin-contracts"
version: "4.5.0"
plugins:
- name: "hardhat"
- name: "solidity"
version: "0.8.1"
test:
number_of_accounts: "{NUMBER_OF_TEST_ACCOUNTS}"
compile:
exclude:
- "exclude_dir"
- "Excl*.json"
- r"Ignore\w*\.json"
""".lstrip()
JSON_CONTENT = f"""
{{
"contracts_folder": "{CONTRACTS_FOLDER}",
"dependencies": [
{{
"name": "openzeppelin",
"github": "OpenZeppelin/openzeppelin-contracts",
"version": "4.5.0"
}}
],
"plugins": [
{{
"name": "hardhat"
}},
{{
"name": "solidity",
"version": "0.8.1"
}}
],
"test": {{
"number_of_accounts": "{NUMBER_OF_TEST_ACCOUNTS}"
}},
"compile": {{
"exclude": [
"exclude_dir",
"Excl*.json",
"r\\"Ignore\\\\w*\\\\.json\\""
]
}}
}}
""".lstrip()
PYPROJECT_TOML = rf"""
[tool.ape]
contracts_folder = "{CONTRACTS_FOLDER}"
[[tool.ape.dependencies]]
name = "openzeppelin"
github = "OpenZeppelin/openzeppelin-contracts"
version = "4.5.0"
[[tool.ape.plugins]]
name = "hardhat"
[[tool.ape.plugins]]
name = "solidity"
version = "0.8.1"
[tool.ape.test]
number_of_accounts = {NUMBER_OF_TEST_ACCOUNTS}
[tool.ape.compile]
exclude = ["exclude_dir", "Excl*.json", 'r"Ignore\w*\.json"']
""".lstrip()
EXT_TO_CONTENT = {
".yml": YAML_CONTENT,
".yaml": YAML_CONTENT,
".json": JSON_CONTENT,
".toml": PYPROJECT_TOML,
}


def test_model_validate_empty():
data: dict = {}
Expand All @@ -41,14 +126,27 @@ def test_model_validate_path_contracts_folder():
assert cfg.contracts_folder == str(path)


def test_validate_file():
value = "pathtowherever"
@pytest.mark.parametrize(
"file", ("ape-config.yml", "ape-config.yaml", "ape-config.json", "pyproject.toml")
)
def test_validate_file(file):
content = EXT_TO_CONTENT[Path(file).suffix]
with create_tempdir() as temp_dir:
file = temp_dir / "ape-config.yaml"
file.write_text(f"contracts_folder: {value}")
actual = ApeConfig.validate_file(file)

assert actual.contracts_folder == value
path = temp_dir / file
path.write_text(content)
actual = ApeConfig.validate_file(path)

assert actual.contracts_folder == CONTRACTS_FOLDER
assert actual.test.number_of_accounts == NUMBER_OF_TEST_ACCOUNTS
assert len(actual.dependencies) == 1
assert actual.dependencies[0]["name"] == "openzeppelin"
assert actual.dependencies[0]["github"] == "OpenZeppelin/openzeppelin-contracts"
assert actual.dependencies[0]["version"] == "4.5.0"
assert actual.plugins == [{"name": "hardhat"}, {"name": "solidity", "version": "0.8.1"}]
assert re.compile("Ignore\\w*\\.json") in actual.compile.exclude
assert "exclude_dir" in actual.compile.exclude
assert ".cache" in actual.compile.exclude
assert "Excl*.json" in actual.compile.exclude


def test_validate_file_expands_env_vars():
Expand Down
Loading

0 comments on commit ca8786a

Please sign in to comment.