Skip to content

Commit

Permalink
feat: --config-override option for compile CLIs (#2064)
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey committed May 7, 2024
1 parent 176dc2a commit 840454f
Show file tree
Hide file tree
Showing 8 changed files with 84 additions and 20 deletions.
29 changes: 19 additions & 10 deletions docs/userguides/compile.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,22 +47,24 @@ If your project includes Solidity (`.sol`) or Vyper (`.vy`) files, you will have
To include additional compilers in your project, you can add the plugins to the `plugins` list in your `ape-config.yaml` or install them using the CLI.
For information on how to configure plugins in your project, follow [this guide](./installing_plugins.html).

## Ignore Files
## Exclude Files

You can configure files to be ignored from compilation.
By default, Ape ignores files `package.json`, `package-lock.json`, `tsconfig.json`.
To override this list, edit your `ape-config.yaml` similarly:
You can configure files to be excluded from compilation.
By default, Ape excludes known non-contract files such as `package.json`, `package-lock.json`, `tsconfig.json`, or `.DS_Store`.
To append file-globs to the exclusions list, edit your `compile:exclude` config like this:

```yaml
compile:
exclude:
- "*package.json"
- "*package-lock.json"
- "*tsconfig.json"
- "*custom.json" # Append a custom ignore
- "examples" # Exclude all files in the examples/ directory
- "*Mock.sol" # Exclude all files ending in Mock.sol
```

**NOTE**: You must include the defaults in the list when overriding if you wish to retain them.
You can also exclude files using the `--config-override` CLI option:

```shell
ape compile --config-override '{"compile": {"exclude": ["*Mock.sol"]}}'
```

## Dependencies

Expand Down Expand Up @@ -108,7 +110,14 @@ vyper:
version: 0.3.10
```

You can also configure adhoc settings in Python code:
When using the CLI, you can also specify settings using the `--config-override`.
This is not limited to compiler settings; you can include other settings, such as `"contracts_folder"`, which affects compiling.

```shell
ape compile --config-override '{"contracts_folder": "other_contracts", "vyper": {"evm_version": "paris"}, "solidity": {"evm_version": "paris"}}'
```

Finally, you can also configure settings in Python code:

```python
from pathlib import Path
Expand Down
6 changes: 6 additions & 0 deletions docs/userguides/dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,12 @@ dependencies:

This is the same as if these values were in an `ape-config.yaml` file in the project directly.

You can also specify `--config-override` in the `ape pm install` command to try different settings more adhoc:

```shell
ape pm install --config-override '{"solidity": {"evm_version": "paris"}}'
```

### Custom Contracts Folder

You can set the name of the dependency's contracts folder using the `config_override` key, e.g.:
Expand Down
22 changes: 18 additions & 4 deletions src/ape/managers/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ class ConfigManager(BaseInterfaceModel):
default_ecosystem: str = "ethereum"
"""The default ecosystem to use. Defaults to ``"ethereum"``."""

_config_override: Dict[str, Any] = {}
"""Adhoc config overrides."""

_cached_configs: Dict[str, Dict[str, Any]] = {}

@model_validator(mode="before")
Expand Down Expand Up @@ -169,7 +172,7 @@ def _plugin_configs(self) -> Dict[str, PluginConfig]:
# NOTE: It is critical that we read in global config values first
# so that project config values will override them as-needed.
project_config = load_config(config_file) if config_file.is_file() else {}
user_config = merge_configs(global_config, project_config)
user_config = merge_configs(global_config, project_config, self._config_override)

self.name = configs["name"] = user_config.pop("name", "")
self.version = configs["version"] = user_config.pop("version", "")
Expand Down Expand Up @@ -265,14 +268,15 @@ def _plugin_configs(self) -> Dict[str, PluginConfig]:
def __repr__(self) -> str:
return f"<{ConfigManager.__name__} project={self.PROJECT_FOLDER.name}>"

def load(self, force_reload: bool = False) -> "ConfigManager":
def load(self, force_reload: bool = False, **overrides) -> "ConfigManager":
"""
Load the user config file and return this class.
"""

if force_reload:
self._cached_configs = {}

self._config_override = overrides or {}
_ = self._plugin_configs

return self
Expand Down Expand Up @@ -374,7 +378,17 @@ def using_project(
config_file.unlink()


def merge_configs(base: Dict, secondary: Dict) -> Dict:
def merge_configs(*cfgs) -> Dict:
if len(cfgs) == 0:
return {}
elif len(cfgs) == 1:
return cfgs[0]

new_base = _merge_configs(cfgs[0], cfgs[1])
return merge_configs(new_base, *cfgs[2:])


def _merge_configs(base: Dict, secondary: Dict) -> Dict:
result: Dict = {}

# Short circuits
Expand All @@ -396,7 +410,7 @@ def merge_configs(base: Dict, secondary: Dict) -> Dict:

else:
# Merge the dictionaries.
sub = merge_configs(base[key], secondary[key])
sub = _merge_configs(base[key], secondary[key])
result[key] = sub

# Add missed keys from secondary.
Expand Down
3 changes: 3 additions & 0 deletions src/ape/managers/project/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ def contracts_folder(self) -> Path:
# Was set explicitly to `None` in config.
return self.path / "contracts"

elif isinstance(folder, str):
return self.path / folder

return folder

@property
Expand Down
18 changes: 15 additions & 3 deletions src/ape_compile/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@
import click
from ethpm_types import ContractType

from ape.cli import ape_cli_context, contract_file_paths_argument
from ape.cli import ape_cli_context, config_override_option, contract_file_paths_argument


def _include_dependencies_callback(ctx, param, value):
return value or ctx.obj.config_manager.get_config("compile").include_dependencies


def _config_override_callback(ctx, param, value):
if value:
ctx.obj.config_manager.load(force_reload=True, **value)


@click.command(short_help="Compile select contract source files")
@ape_cli_context()
@contract_file_paths_argument()
Expand All @@ -37,15 +42,22 @@ def _include_dependencies_callback(ctx, param, value):
help="Also compile dependencies",
callback=_include_dependencies_callback,
)
def cli(cli_ctx, file_paths: Set[Path], use_cache: bool, display_size: bool, include_dependencies):
@config_override_option(callback=_config_override_callback)
def cli(
cli_ctx,
file_paths: Set[Path],
use_cache: bool,
display_size: bool,
include_dependencies,
config_override,
):
"""
Compiles the manifest for this project and saves the results
back to the manifest.
Note that ape automatically recompiles any changed contracts each time
a project is loaded. You do not have to manually trigger a recompile.
"""

sources_missing = cli_ctx.project_manager.sources_missing
if not file_paths and sources_missing and len(cli_ctx.project_manager.dependencies) == 0:
cli_ctx.logger.warning("Nothing to compile.")
Expand Down
7 changes: 6 additions & 1 deletion src/ape_pm/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,8 @@ def remove(cli_ctx, package, versions, yes):
@click.argument("name", nargs=1, required=False)
@click.option("--version", help="The dependency version", metavar="VERSION")
@click.option("--force", "-f", help="Force a re-compile", is_flag=True)
def compile(cli_ctx, name, version, force):
@config_override_option()
def compile(cli_ctx, name, version, force, config_override):
"""
Compile a package
"""
Expand All @@ -325,6 +326,8 @@ def compile(cli_ctx, name, version, force):
if version != "local":
log_line += f"@{version}"

if config_override:
dependency.config_override = config_override
try:
dependency.compile(use_cache=not force)
except Exception as err:
Expand Down Expand Up @@ -366,6 +369,8 @@ def compile(cli_ctx, name, version, force):
cli_ctx.abort(f"Version '{version}' for dependency '{name}' not found. Is it installed?")

dependency = versions[version_found]
if config_override:
dependency.config_override = config_override

try:
dependency.compile(use_cache=not force)
Expand Down
12 changes: 12 additions & 0 deletions tests/integration/cli/test_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,3 +366,15 @@ def test_compile_exclude(ape_cli, runner):
result = runner.invoke(ape_cli, ("compile", "--force"), catch_exceptions=False)
assert "Compiling 'Exclude.json'" not in result.output
assert "Compiling 'exclude_dir/UnwantedContract.json'" not in result.output


@skip_projects_except("with-contracts")
def test_compile_config_override(ape_cli, runner):
arguments = (
"compile",
"--force",
"--config-override",
'{"compile": {"exclude": ["*ContractA*"]}}',
)
result = runner.invoke(ape_cli, arguments, catch_exceptions=False)
assert "Compiling 'ContractA.json'" not in result.output
7 changes: 5 additions & 2 deletions tests/integration/cli/test_pm.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,12 @@ def test_compile(ape_cli, runner, project):


@skip_projects_except("with-contracts")
def test_compile_dependency(ape_cli, runner, project):
def test_compile_config_override(ape_cli, runner, project):
name = "foodep"
result = runner.invoke(ape_cli, ("pm", "compile", name))
result = runner.invoke(
ape_cli,
("pm", "compile", name, "--force", "--config-override", '{"contracts_folder": "src"}'),
)
assert result.exit_code == 0, result.output
assert f"Package '{name}' compiled." in result.output

Expand Down

0 comments on commit 840454f

Please sign in to comment.