diff --git a/docs/userguides/config.md b/docs/userguides/config.md index 24182fffa8..9e8809d928 100644 --- a/docs/userguides/config.md +++ b/docs/userguides/config.md @@ -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) @@ -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 @@ -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 ``` @@ -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 ``` @@ -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 ``` @@ -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 ``` @@ -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: @@ -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: @@ -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 @@ -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 diff --git a/src/ape/api/config.py b/src/ape/api/config.py index 3138390b47..f1cb66f51e 100644 --- a/src/ape/api/config.py +++ b/src/ape/api/config.py @@ -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 diff --git a/src/ape/api/projects.py b/src/ape/api/projects.py index 9017c07149..695dbad32d 100644 --- a/src/ape/api/projects.py +++ b/src/ape/api/projects.py @@ -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) diff --git a/src/ape/managers/project.py b/src/ape/managers/project.py index 0d0c4d9a8b..f84e19e67a 100644 --- a/src/ape/managers/project.py +++ b/src/ape/managers/project.py @@ -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) diff --git a/src/ape/utils/misc.py b/src/ape/utils/misc.py index 81e8b33c56..a068786e87 100644 --- a/src/ape/utils/misc.py +++ b/src/ape/utils/misc.py @@ -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 @@ -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) diff --git a/src/ape_pm/_cli.py b/src/ape_pm/_cli.py index 8fe0e0c509..78326521c5 100644 --- a/src/ape_pm/_cli.py +++ b/src/ape_pm/_cli.py @@ -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() diff --git a/src/ape_pm/dependency.py b/src/ape_pm/dependency.py index 650ba482ac..5d2c336266 100644 --- a/src/ape_pm/dependency.py +++ b/src/ape_pm/dependency.py @@ -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 diff --git a/tests/functional/test_config.py b/tests/functional/test_config.py index 1cfef6716b..7612446f23 100644 --- a/tests/functional/test_config.py +++ b/tests/functional/test_config.py @@ -1,4 +1,5 @@ import os +import re from pathlib import Path from typing import Optional, Union @@ -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 = {} @@ -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(): diff --git a/tests/integration/cli/projects/with-contracts/ape-config.yaml b/tests/integration/cli/projects/with-contracts/ape-config.yaml deleted file mode 100644 index b0848b112e..0000000000 --- a/tests/integration/cli/projects/with-contracts/ape-config.yaml +++ /dev/null @@ -1,25 +0,0 @@ -name: withcontracts - -dependencies: - - name: foodep - local: ./dep - - # Showing we can have dependencies with contracts_folder - # equal to the path. (used in `tests/integration/cli/test_pm.py -k test_install`) - - name: depcontractsfolderroot - local: ./dep_contracts_folder_root - config_override: - contracts_folder: . - - - name: depwithunregisteredcontracts - local: ./dep_with_sol_and_vy - -test: - # `false` because running pytest within pytest. - disconnect_providers_after: false - -compile: - exclude: - - exclude_dir - - Excl*.json - - r"Ignore\w*\.json" diff --git a/tests/integration/cli/projects/with-contracts/pyproject.toml b/tests/integration/cli/projects/with-contracts/pyproject.toml new file mode 100644 index 0000000000..e761a00178 --- /dev/null +++ b/tests/integration/cli/projects/with-contracts/pyproject.toml @@ -0,0 +1,23 @@ +[tool.ape] +name = "withcontracts" + +[[tool.ape.dependencies]] +name = "foodep" +local = "./dep" + +[[tool.ape.dependencies]] +name = "depcontractsfolderroot" +local = "./dep_contracts_folder_root" + +[tool.ape.dependencies.config_override] +contracts_folder = "." + +[[tool.ape.dependencies]] +name = "depwithunregisteredcontracts" +local = "./dep_with_sol_and_vy" + +[tool.ape.test] +disconnect_providers_after = false + +[tool.ape.compile] +exclude = ["exclude_dir", "Excl*.json", 'r"Ignore\w*\.json"'] diff --git a/tests/integration/cli/test_pm.py b/tests/integration/cli/test_pm.py index 77ddcca0aa..8b51fcfd32 100644 --- a/tests/integration/cli/test_pm.py +++ b/tests/integration/cli/test_pm.py @@ -42,11 +42,12 @@ def test_install_path_to_local_package(pm_runner, integ_project): @run_once def test_install_path_to_local_config_file(pm_runner): project = "with-contracts" - path = Path(__file__).parent / "projects" / project / "ape-config.yaml" + path = Path(__file__).parent / "projects" / project / "pyproject.toml" arguments = ("install", path.as_posix(), "--name", project) result = pm_runner.invoke(*arguments) assert result.exit_code == 0, result.output - assert f"Package '{path.parent.as_posix()}' installed." + assert "SUCCESS" in result.output + assert "Package 'with-contracts@local' installed." in result.output @skip_projects_except("test", "with-contracts")