Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adhoc compiler settings [APE-1474] #1705

Merged
merged 4 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/userguides/compile.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,32 @@ Alternatively, configure it to always happen:
compile:
use_dependencies: true
```

## Settings

Generally, configure compiler plugins using your `ape-config.yaml` file.
For example, when using the `vyper` plugin, you can configure settings under the `vyper` key:

```yaml
vyper:
version: 0.3.10
```

You can also configure adhoc settings in Python code:

```python
from pathlib import Path
from ape import compilers

settings = {"vyper": {"version": "0.3.7"}, "solidity": {"version": "0.8.0"}}
compilers.compile(
sabotagebeats marked this conversation as resolved.
Show resolved Hide resolved
["path/to/contract.vy", "path/to/contract.sol"], settings=settings
)

# Or, more explicitly:
vyper = compilers.get_compiler("vyper", settings=settings["vyper"])
vyper.compile([Path("path/to/contract.vy")])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

works for me!


solidity = compilers.get_compiler("solidity", settings=settings["solidity"])
vyper.compile([Path("path/to/contract.sol")])
```
22 changes: 22 additions & 0 deletions src/ape/api/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from evm_trace.geth import create_call_node_data
from semantic_version import Version # type: ignore

from ape.api.config import PluginConfig
from ape.exceptions import APINotImplementedError, ContractLogicError
from ape.types.coverage import ContractSourceCoverage
from ape.types.trace import SourceTraceback, TraceFrame
Expand All @@ -25,11 +26,32 @@ class CompilerAPI(BaseInterfaceModel):
this API.
"""

compiler_settings: Dict = {}
"""
Adhoc compiler settings.
"""

@property
@abstractmethod
def name(self) -> str:
...

@property
def config(self) -> PluginConfig:
"""
The provider's configuration.
"""
return self.config_manager.get_config(self.name)

@property
def settings(self) -> PluginConfig:
"""
The combination of settings from ``ape-config.yaml`` and ``.compiler_settings``.
"""
CustomConfig = self.config.__class__
data = {**self.config.dict(), **self.compiler_settings}
return CustomConfig.parse_obj(data)

@abstractmethod
def get_versions(self, all_paths: List[Path]) -> Set[str]:
"""
Expand Down
45 changes: 31 additions & 14 deletions src/ape/managers/compilers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from pathlib import Path
from typing import Any, Dict, List, Optional, Set
from typing import Any, Dict, List, Optional, Sequence, Set, Union

from ethpm_types import ContractType
from ethpm_types.source import Content
Expand Down Expand Up @@ -72,14 +72,22 @@ def registered_compilers(self) -> Dict[str, CompilerAPI]:
self._registered_compilers_cache[cache_key] = registered_compilers
return registered_compilers

def get_compiler(self, name: str) -> Optional[CompilerAPI]:
def get_compiler(self, name: str, settings: Optional[Dict] = None) -> Optional[CompilerAPI]:
for compiler in self.registered_compilers.values():
if compiler.name == name:
return compiler
if compiler.name != name:
continue

if settings is not None and settings != compiler.compiler_settings:
# Use a new instance to support multiple compilers of same type.
return compiler.copy(update={"compiler_settings": settings})

return compiler

return None

def compile(self, contract_filepaths: List[Path]) -> Dict[str, ContractType]:
def compile(
self, contract_filepaths: Sequence[Union[Path, str]], settings: Optional[Dict] = None
) -> Dict[str, ContractType]:
"""
Invoke :meth:`ape.ape.compiler.CompilerAPI.compile` for each of the given files.
For example, use the `ape-solidity plugin <https://github.com/ApeWorX/ape-solidity>`__
Expand All @@ -90,13 +98,17 @@ def compile(self, contract_filepaths: List[Path]) -> Dict[str, ContractType]:
extension as well as when there is a contract-type collision across compilers.

Args:
contract_filepaths (List[pathlib.Path]): The list of files to compile,
as ``pathlib.Path`` objects.
contract_filepaths (Sequence[Union[pathlib.Path], str]): The list of files to compile,
as ``pathlib.Path`` objects. You can also pass a list of `str` that will
automatically get turned to ``pathlib.Path`` objects.
settings (Optional[Dict]): Adhoc compiler settings. Defaults to None.
Ensure the compiler name key is present in the dict for it to work.

Returns:
Dict[str, ``ContractType``]: A mapping of contract names to their type.
"""
extensions = self._get_contract_extensions(contract_filepaths)
contract_file_paths = [Path(p) if isinstance(p, str) else p for p in contract_filepaths]
extensions = self._get_contract_extensions(contract_file_paths)
contracts_folder = self.config_manager.contracts_folder
contract_types_dict: Dict[str, ContractType] = {}
built_paths = [p for p in self.project_manager.local_project._cache_folder.glob("*.json")]
Expand All @@ -114,7 +126,7 @@ def compile(self, contract_filepaths: List[Path]) -> Dict[str, ContractType]:
# Filter out in-source cache files from dependencies.
paths_to_compile = [
path
for path in contract_filepaths
for path in contract_file_paths
if path.is_file()
and path not in paths_to_ignore
and path not in built_paths
Expand All @@ -126,9 +138,14 @@ def compile(self, contract_filepaths: List[Path]) -> Dict[str, ContractType]:
for source_id in source_ids:
logger.info(f"Compiling '{source_id}'.")

compiled_contracts = self.registered_compilers[extension].compile(
paths_to_compile, base_path=contracts_folder
)
name = self.registered_compilers[extension].name
compiler = self.get_compiler(name, settings=settings)
if compiler is None:
# For mypy - should not be possible.
raise ValueError("Compiler should not be None")

compiled_contracts = compiler.compile(paths_to_compile, base_path=contracts_folder)

for contract_type in compiled_contracts:
contract_name = contract_type.name
if not contract_name:
Expand Down Expand Up @@ -180,14 +197,14 @@ def compile(self, contract_filepaths: List[Path]) -> Dict[str, ContractType]:
return contract_types_dict

def get_imports(
self, contract_filepaths: List[Path], base_path: Optional[Path] = None
self, contract_filepaths: Sequence[Path], base_path: Optional[Path] = None
) -> Dict[str, List[str]]:
"""
Combine import dicts from all compilers, where the key is a contract's source_id
and the value is a list of import source_ids.

Args:
contract_filepaths (List[pathlib.Path]): A list of source file paths to compile.
contract_filepaths (Sequence[pathlib.Path]): A list of source file paths to compile.
base_path (Optional[pathlib.Path]): Optionally provide the base path, such as the
project ``contracts/`` directory. Defaults to ``None``. When using in a project
via ``ape compile``, gets set to the project's ``contracts/`` directory.
Expand Down
4 changes: 3 additions & 1 deletion tests/functional/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -661,9 +661,11 @@ def method_abi_with_struct_input():
@pytest.fixture
def mock_compiler(mocker):
mock = mocker.MagicMock()
mock.name = "mock"
mock.ext = ".__mock__"

def mock_compile(paths, *args, **kwargs):
def mock_compile(paths, base_path=None):
mock.tracked_settings.append(mock.compiler_settings)
result = []
for path in paths:
if path.suffix == mock.ext:
Expand Down
41 changes: 41 additions & 0 deletions tests/functional/test_compilers.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,22 @@ def test_get_compiler(compilers):
assert compilers.get_compiler("foobar") is None


def test_get_compiler_with_settings(compilers, mock_compiler, project_with_contract):
existing_compilers = compilers._registered_compilers_cache[project_with_contract.path]
all_compilers = {**existing_compilers, mock_compiler.ext: mock_compiler}

try:
compilers._registered_compilers_cache[project_with_contract.path] = all_compilers
compiler_0 = compilers.get_compiler("mock", settings={"bar": "foo"})
compiler_1 = compiler_0.get_compiler("mock", settings={"foo": "bar"})

finally:
compilers._registered_compilers_cache[project_with_contract.path] = existing_compilers

assert compiler_0.compiler_settings != compiler_1.compiler_settings
assert id(compiler_0) != id(compiler_1)


def test_getattr(compilers):
compiler = compilers.ethpm
assert compiler.name == "ethpm"
Expand Down Expand Up @@ -89,3 +105,28 @@ def test_contract_type_collision(compilers, project_with_contract, mock_compiler

finally:
compilers._registered_compilers_cache[project_with_contract.path] = existing_compilers


def test_compile_with_settings(mock_compiler, compilers, project_with_contract):
existing_compilers = compilers._registered_compilers_cache[project_with_contract.path]
all_compilers = {**existing_compilers, mock_compiler.ext: mock_compiler}
new_contract = project_with_contract.path / f"AMockContract{mock_compiler.ext}"
new_contract.write_text("foobar")
settings = {"mock": {"foo": "bar"}}

try:
compilers._registered_compilers_cache[project_with_contract.path] = all_compilers
compilers.compile([new_contract], settings=settings)

finally:
compilers._registered_compilers_cache[project_with_contract.path] = existing_compilers

actual = mock_compiler.method_calls[0][2]["update"]["compiler_settings"]["mock"]
assert actual == settings["mock"]


def test_compile_str_path(compilers, project_with_contract):
path = next(iter(project_with_contract.source_paths))
actual = compilers.compile([str(path)])
contract_name = path.stem
assert actual[contract_name].name == contract_name
Loading