From f0b95336d54b7089fc2b9bb29462359f98854cef Mon Sep 17 00:00:00 2001 From: z80 Date: Mon, 23 Oct 2023 23:38:48 -0400 Subject: [PATCH 01/16] feat: support new pragma formats --- ape_vyper/compiler.py | 195 +++++++++++++++++++++++++----------------- 1 file changed, 116 insertions(+), 79 deletions(-) diff --git a/ape_vyper/compiler.py b/ape_vyper/compiler.py index a1284ac6..4efa00f1 100644 --- a/ape_vyper/compiler.py +++ b/ape_vyper/compiler.py @@ -68,9 +68,9 @@ def _install_vyper(version: Version): ) from err -def get_pragma_spec(source: Union[str, Path]) -> Optional[SpecifierSet]: +def get_version_pragma_spec(source: Union[str, Path]) -> Optional[SpecifierSet]: """ - Extracts pragma information from Vyper source code. + Extracts version pragma information from Vyper source code. Args: source (str): Vyper source code @@ -81,7 +81,10 @@ def get_pragma_spec(source: Union[str, Path]) -> Optional[SpecifierSet]: source_str = source if isinstance(source, str) else source.read_text() pragma_match = next(re.finditer(r"(?:\n|^)\s*#\s*@version\s*([^\n]*)", source_str), None) if pragma_match is None: - return None # Try compiling with latest + # support new pragma syntax + pragma_match = next(re.finditer(r"(?:\n|^)\s*#pragma\s+version\s*([^\n]*)", source_str), None) + if pragma_match is None: + return None # Try compiling with latest raw_pragma = pragma_match.groups()[0] pragma_str = " ".join(raw_pragma.split()).replace("^", "~=") @@ -94,6 +97,22 @@ def get_pragma_spec(source: Union[str, Path]) -> Optional[SpecifierSet]: logger.warning(f"Invalid pragma spec: '{raw_pragma}'. Trying latest.") return None +def get_optimization_pragma(source: Union[str, Path]) -> Optional[str | bool]: + """ + Extracts optimization pragma information from Vyper source code. + + Args: + source (str): Vyper source code + Returns: + ``str``, or True if no valid pragma is found (for backwards compatibility). + """ + source_str = source if isinstance(source, str) else source.read_text() + pragma_match = next(re.finditer(r"(?:\n|^)\s*#pragma\s+optimize\s+([^\n]*)", source_str), None) + if pragma_match is None: + return True + return pragma_match.groups()[0] + + class VyperCompiler(CompilerAPI): @property @@ -145,7 +164,7 @@ def get_imports( def get_versions(self, all_paths: List[Path]) -> Set[str]: versions = set() for path in all_paths: - if version_spec := get_pragma_spec(path): + if version_spec := get_version_pragma_spec(path): try: # Make sure we have the best compiler available to compile this version_iter = version_spec.filter(self.available_versions) @@ -283,88 +302,106 @@ def compile( for vyper_version, source_paths in version_map.items(): settings = all_settings.get(vyper_version, {}) path_args = {str(get_relative_path(p.absolute(), base_path)): p for p in source_paths} - input_json = { - "language": "Vyper", - "settings": settings, - "sources": {s: {"content": p.read_text()} for s, p in path_args.items()}, - } - if interfaces := self.import_remapping: - input_json["interfaces"] = interfaces - - vyper_binary = compiler_data[vyper_version]["vyper_binary"] - try: - result = vvm.compile_standard( - input_json, - base_path=base_path, - vyper_version=vyper_version, - vyper_binary=vyper_binary, - ) - except VyperError as err: - raise VyperCompileError(err) from err + optimizations_map = self.get_optimization_pragma_map(list(source_paths)) - def classify_ast(_node: ASTNode): - if _node.ast_type in _FUNCTION_AST_TYPES: - _node.classification = ASTClassification.FUNCTION + for optimization, paths in optimizations_map.items(): + input_json = { + "language": "Vyper", + "settings": settings, + "sources": {s: {"content": p.read_text()} for s, p in path_args.items()}, + } - for child in _node.children: - classify_ast(child) + input_json["settings"]["optimize"] = optimization + if interfaces := self.import_remapping: + input_json["interfaces"] = interfaces - for source_id, output_items in result["contracts"].items(): - content = { - i + 1: ln - for i, ln in enumerate((base_path / source_id).read_text().splitlines()) - } - for name, output in output_items.items(): - # De-compress source map to get PC POS map. - ast = ASTNode.parse_obj(result["sources"][source_id]["ast"]) - classify_ast(ast) - - # Track function offsets. - function_offsets = [] - for node in ast.children: - lineno = node.lineno - - # NOTE: Constructor is handled elsewhere. - if node.ast_type == "FunctionDef" and "__init__" not in content.get( - lineno, "" - ): - function_offsets.append((node.lineno, node.end_lineno)) - - evm = output["evm"] - bytecode = evm["deployedBytecode"] - opcodes = bytecode["opcodes"].split(" ") - compressed_src_map = SourceMap(__root__=bytecode["sourceMap"]) - src_map = list(compressed_src_map.parse())[1:] - - pcmap = ( - _get_legacy_pcmap(ast, src_map, opcodes) - if vyper_version <= Version("0.3.7") - else _get_pcmap(bytecode) + vyper_binary = compiler_data[vyper_version]["vyper_binary"] + try: + result = vvm.compile_standard( + input_json, + base_path=base_path, + vyper_version=vyper_version, + vyper_binary=vyper_binary, ) + except VyperError as err: + raise VyperCompileError(err) from err + + def classify_ast(_node: ASTNode): + if _node.ast_type in _FUNCTION_AST_TYPES: + _node.classification = ASTClassification.FUNCTION + + for child in _node.children: + classify_ast(child) + + for source_id, output_items in result["contracts"].items(): + content = { + i + 1: ln + for i, ln in enumerate((base_path / source_id).read_text().splitlines()) + } + for name, output in output_items.items(): + # De-compress source map to get PC POS map. + ast = ASTNode.parse_obj(result["sources"][source_id]["ast"]) + classify_ast(ast) + + # Track function offsets. + function_offsets = [] + for node in ast.children: + lineno = node.lineno + + # NOTE: Constructor is handled elsewhere. + if node.ast_type == "FunctionDef" and "__init__" not in content.get( + lineno, "" + ): + function_offsets.append((node.lineno, node.end_lineno)) + + evm = output["evm"] + bytecode = evm["deployedBytecode"] + opcodes = bytecode["opcodes"].split(" ") + compressed_src_map = SourceMap(__root__=bytecode["sourceMap"]) + src_map = list(compressed_src_map.parse())[1:] + + pcmap = ( + _get_legacy_pcmap(ast, src_map, opcodes) + if vyper_version <= Version("0.3.7") + else _get_pcmap(bytecode) + ) - # Find content-specified dev messages. - dev_messages = {} - for line_no, line in content.items(): - if match := re.search(DEV_MSG_PATTERN, line): - dev_messages[line_no] = match.group(1).strip() - - contract_type = ContractType( - ast=ast, - contractName=name, - sourceId=source_id, - deploymentBytecode={"bytecode": evm["bytecode"]["object"]}, - runtimeBytecode={"bytecode": bytecode["object"]}, - abi=output["abi"], - sourcemap=compressed_src_map, - pcmap=pcmap, - userdoc=output["userdoc"], - devdoc=output["devdoc"], - dev_messages=dev_messages, - ) - contract_types.append(contract_type) + # Find content-specified dev messages. + dev_messages = {} + for line_no, line in content.items(): + if match := re.search(DEV_MSG_PATTERN, line): + dev_messages[line_no] = match.group(1).strip() + + contract_type = ContractType( + ast=ast, + contractName=name, + sourceId=source_id, + deploymentBytecode={"bytecode": evm["bytecode"]["object"]}, + runtimeBytecode={"bytecode": bytecode["object"]}, + abi=output["abi"], + sourcemap=compressed_src_map, + pcmap=pcmap, + userdoc=output["userdoc"], + devdoc=output["devdoc"], + dev_messages=dev_messages, + ) + contract_types.append(contract_type) return contract_types + def get_optimization_pragma_map( + self, contract_filepaths: List[Path], base_path: Optional[Path] = None + ) -> Dict[str, Set[Path]]: + base_path = base_path or self.config_manager.contracts_folder + optimization_pragma_map: Dict[str, Set[Path]] = {} + for path in contract_filepaths: + if pragma := get_optimization_pragma(path): + if pragma not in optimization_pragma_map: + optimization_pragma_map[pragma] = set() + optimization_pragma_map[pragma].add(path) + + return optimization_pragma_map + def get_version_map( self, contract_filepaths: List[Path], base_path: Optional[Path] = None ) -> Dict[Version, Set[Path]]: @@ -374,7 +411,7 @@ def get_version_map( # Sort contract_filepaths to promote consistent, reproduce-able behavior for path in sorted(contract_filepaths): - if pragma := get_pragma_spec(path): + if pragma := get_version_pragma_spec(path): _safe_append(source_path_by_pragma_spec, pragma, path) else: source_paths_without_pragma.add(path) From 145d376ea5dd50eb2c5007325e479781bd553e1e Mon Sep 17 00:00:00 2001 From: z80 Date: Mon, 23 Oct 2023 23:39:29 -0400 Subject: [PATCH 02/16] fix: lint --- ape_vyper/compiler.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ape_vyper/compiler.py b/ape_vyper/compiler.py index 4efa00f1..a3a5068c 100644 --- a/ape_vyper/compiler.py +++ b/ape_vyper/compiler.py @@ -82,7 +82,9 @@ def get_version_pragma_spec(source: Union[str, Path]) -> Optional[SpecifierSet]: pragma_match = next(re.finditer(r"(?:\n|^)\s*#\s*@version\s*([^\n]*)", source_str), None) if pragma_match is None: # support new pragma syntax - pragma_match = next(re.finditer(r"(?:\n|^)\s*#pragma\s+version\s*([^\n]*)", source_str), None) + pragma_match = next( + re.finditer(r"(?:\n|^)\s*#pragma\s+version\s*([^\n]*)", source_str), None + ) if pragma_match is None: return None # Try compiling with latest @@ -97,6 +99,7 @@ def get_version_pragma_spec(source: Union[str, Path]) -> Optional[SpecifierSet]: logger.warning(f"Invalid pragma spec: '{raw_pragma}'. Trying latest.") return None + def get_optimization_pragma(source: Union[str, Path]) -> Optional[str | bool]: """ Extracts optimization pragma information from Vyper source code. @@ -113,7 +116,6 @@ def get_optimization_pragma(source: Union[str, Path]) -> Optional[str | bool]: return pragma_match.groups()[0] - class VyperCompiler(CompilerAPI): @property def config(self) -> VyperConfig: From 6cb728e2723f068cfb736a87c3aca8ca3a348a63 Mon Sep 17 00:00:00 2001 From: z80 Date: Mon, 23 Oct 2023 23:57:21 -0400 Subject: [PATCH 03/16] fix: properly handle rest of settings needed for per-optimization-level compilation --- ape_vyper/compiler.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/ape_vyper/compiler.py b/ape_vyper/compiler.py index a3a5068c..3899bc19 100644 --- a/ape_vyper/compiler.py +++ b/ape_vyper/compiler.py @@ -303,10 +303,12 @@ def compile( for vyper_version, source_paths in version_map.items(): settings = all_settings.get(vyper_version, {}) - path_args = {str(get_relative_path(p.absolute(), base_path)): p for p in source_paths} optimizations_map = self.get_optimization_pragma_map(list(source_paths)) - for optimization, paths in optimizations_map.items(): + for optimization, source_paths in optimizations_map.items(): + path_args = { + str(get_relative_path(p.absolute(), base_path)): p for p in source_paths + } input_json = { "language": "Vyper", "settings": settings, @@ -314,6 +316,7 @@ def compile( } input_json["settings"]["optimize"] = optimization + input_json["settings"]["outputSelection"] = {s: ["*"] for s in path_args} if interfaces := self.import_remapping: input_json["interfaces"] = interfaces @@ -480,10 +483,6 @@ def get_compiler_settings( continue version_settings: Dict = {"optimize": True} - path_args = { - str(get_relative_path(p.absolute(), contracts_path)): p for p in source_paths - } - version_settings["outputSelection"] = {s: ["*"] for s in path_args} if evm_version := data.get("evm_version"): version_settings["evmVersion"] = evm_version From ae20d693985f26889bd4ee8cfece220ccc3cb41a Mon Sep 17 00:00:00 2001 From: z80 Date: Tue, 24 Oct 2023 00:04:18 -0400 Subject: [PATCH 04/16] fix: typ hint --- ape_vyper/compiler.py | 2 +- tests/contracts/passing_contracts/optimize_codesize.vy | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 tests/contracts/passing_contracts/optimize_codesize.vy diff --git a/ape_vyper/compiler.py b/ape_vyper/compiler.py index 3899bc19..4abcf821 100644 --- a/ape_vyper/compiler.py +++ b/ape_vyper/compiler.py @@ -100,7 +100,7 @@ def get_version_pragma_spec(source: Union[str, Path]) -> Optional[SpecifierSet]: return None -def get_optimization_pragma(source: Union[str, Path]) -> Optional[str | bool]: +def get_optimization_pragma(source: Union[str, Path]) -> str | bool | None: """ Extracts optimization pragma information from Vyper source code. diff --git a/tests/contracts/passing_contracts/optimize_codesize.vy b/tests/contracts/passing_contracts/optimize_codesize.vy new file mode 100644 index 00000000..754f0ff0 --- /dev/null +++ b/tests/contracts/passing_contracts/optimize_codesize.vy @@ -0,0 +1,8 @@ +#pragma version 0.3.10 +#pragma optimize codesize + +x: uint256 + +@external +def __init__(): + self.x = 0 From 8552689440f0c979cc33b81d97658485bd983c78 Mon Sep 17 00:00:00 2001 From: z80 Date: Tue, 24 Oct 2023 00:09:55 -0400 Subject: [PATCH 05/16] fix: type hint should use Union --- ape_vyper/compiler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ape_vyper/compiler.py b/ape_vyper/compiler.py index 4abcf821..5bf5e327 100644 --- a/ape_vyper/compiler.py +++ b/ape_vyper/compiler.py @@ -100,7 +100,7 @@ def get_version_pragma_spec(source: Union[str, Path]) -> Optional[SpecifierSet]: return None -def get_optimization_pragma(source: Union[str, Path]) -> str | bool | None: +def get_optimization_pragma(source: Union[str, Path]) -> Union[str, bool]: """ Extracts optimization pragma information from Vyper source code. From 9b05ad30a81053b8aec2a23afebca53021951c80 Mon Sep 17 00:00:00 2001 From: z80 Date: Tue, 24 Oct 2023 00:25:03 -0400 Subject: [PATCH 06/16] fix: make mypy happy --- ape_vyper/compiler.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/ape_vyper/compiler.py b/ape_vyper/compiler.py index 5bf5e327..ed25fb13 100644 --- a/ape_vyper/compiler.py +++ b/ape_vyper/compiler.py @@ -302,21 +302,22 @@ def compile( all_settings = self.get_compiler_settings(sources, base_path=base_path) for vyper_version, source_paths in version_map.items(): - settings = all_settings.get(vyper_version, {}) + version_settings = all_settings.get(vyper_version, {}) optimizations_map = self.get_optimization_pragma_map(list(source_paths)) for optimization, source_paths in optimizations_map.items(): + settings: Dict[str, Any] = version_settings.copy() + settings["optimize"] = optimization path_args = { str(get_relative_path(p.absolute(), base_path)): p for p in source_paths } + settings["outputSelection"] = {s: ["*"] for s in path_args} input_json = { "language": "Vyper", "settings": settings, "sources": {s: {"content": p.read_text()} for s, p in path_args.items()}, } - input_json["settings"]["optimize"] = optimization - input_json["settings"]["outputSelection"] = {s: ["*"] for s in path_args} if interfaces := self.import_remapping: input_json["interfaces"] = interfaces @@ -396,9 +397,9 @@ def classify_ast(_node: ASTNode): def get_optimization_pragma_map( self, contract_filepaths: List[Path], base_path: Optional[Path] = None - ) -> Dict[str, Set[Path]]: + ) -> Dict[Union[str, bool], Set[Path]]: base_path = base_path or self.config_manager.contracts_folder - optimization_pragma_map: Dict[str, Set[Path]] = {} + optimization_pragma_map: Dict[Union[str, bool], Set[Path]] = {} for path in contract_filepaths: if pragma := get_optimization_pragma(path): if pragma not in optimization_pragma_map: From 70f21c9c1a7d2b6941ef1682f108b1870a4d5b71 Mon Sep 17 00:00:00 2001 From: z80 Date: Tue, 24 Oct 2023 13:24:02 -0400 Subject: [PATCH 07/16] feat: update tests to support 0.3.10 --- ape_vyper/compiler.py | 2 +- ape_vyper/exceptions.py | 2 +- tests/conftest.py | 6 +++--- .../contract_with_dev_messages.vy | 2 +- tests/test_ape_reverts.py | 2 +- tests/test_compiler.py | 16 +++++++++++----- 6 files changed, 18 insertions(+), 12 deletions(-) diff --git a/ape_vyper/compiler.py b/ape_vyper/compiler.py index ed25fb13..23c5c20c 100644 --- a/ape_vyper/compiler.py +++ b/ape_vyper/compiler.py @@ -994,7 +994,7 @@ def _get_pcmap(bytecode: Dict) -> PCMap: error_str = RuntimeErrorType.FALLBACK_NOT_DEFINED.value use_loc = False elif "bad calldatasize or callvalue" in error_type: - # Only on >=0.3.10rc3. + # Only on >=0.3.10. # NOTE: We are no longer able to get Nonpayable checks errors since they # are now combined. error_str = RuntimeErrorType.INVALID_CALLDATA_OR_VALUE.value diff --git a/ape_vyper/exceptions.py b/ape_vyper/exceptions.py index d70f3427..b3467af9 100644 --- a/ape_vyper/exceptions.py +++ b/ape_vyper/exceptions.py @@ -86,7 +86,7 @@ def __init__(self, **kwargs): class InvalidCalldataOrValueError(VyperRuntimeError): """ - Raises on Vyper versions >= 0.3.10rc3 in place of NonPayableError. + Raises on Vyper versions >= 0.3.10 in place of NonPayableError. """ def __init__(self, **kwargs): diff --git a/tests/conftest.py b/tests/conftest.py index 616edad7..dd803fba 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -35,14 +35,14 @@ "0.3.4", "0.3.7", "0.3.9", - "0.3.10rc3", + "0.3.10", ) CONTRACT_VERSION_GEN_MAP = { "": ( "0.3.7", "0.3.9", - "0.3.10rc3", + "0.3.10", ), "sub_reverts": ALL_VERSIONS, } @@ -188,7 +188,7 @@ def account(): return ape.accounts.test_accounts[0] -@pytest.fixture(params=("037", "039", "0310rc3")) +@pytest.fixture(params=("037", "039", "0310")) def traceback_contract(request, account, project, geth_provider): return _get_tb_contract(request.param, project, account) diff --git a/tests/contracts/passing_contracts/contract_with_dev_messages.vy b/tests/contracts/passing_contracts/contract_with_dev_messages.vy index 04c39a81..9655de45 100644 --- a/tests/contracts/passing_contracts/contract_with_dev_messages.vy +++ b/tests/contracts/passing_contracts/contract_with_dev_messages.vy @@ -1,4 +1,4 @@ -# @version 0.3.9 +# @version 0.3.10 # Test dev messages in various code placements @external diff --git a/tests/test_ape_reverts.py b/tests/test_ape_reverts.py index 1c1e7d70..abebef26 100644 --- a/tests/test_ape_reverts.py +++ b/tests/test_ape_reverts.py @@ -10,7 +10,7 @@ def older_reverts_contract(account, project, geth_provider, request): return container.deploy(sender=account) -@pytest.fixture(params=("037", "039", "0310rc3")) +@pytest.fixture(params=("037", "039", "0310")) def reverts_contract_instance(account, project, geth_provider, request): sub_reverts_container = project.get_contract(f"sub_reverts_{request.param}") sub_reverts = sub_reverts_container.deploy(sender=account) diff --git a/tests/test_compiler.py b/tests/test_compiler.py index a6a107ca..93b8afbf 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -1,4 +1,5 @@ import re +import pprint import pytest from ape.exceptions import ContractLogicError @@ -21,7 +22,7 @@ OLDER_VERSION_FROM_PRAGMA = Version("0.2.16") VERSION_37 = Version("0.3.7") -VERSION_FROM_PRAGMA = Version("0.3.9") +VERSION_FROM_PRAGMA = Version("0.3.10") @pytest.fixture @@ -80,7 +81,9 @@ def test_get_version_map(project, compiler, all_versions): x for x in project.contracts_folder.iterdir() if x.is_file() and x.suffix == ".vy" ] actual = compiler.get_version_map(vyper_files) + pprint.pprint(actual) expected_versions = [Version(v) for v in all_versions] + pprint.pprint(expected_versions) for version, sources in actual.items(): if version in expected_versions: @@ -98,12 +101,15 @@ def test_get_version_map(project, compiler, all_versions): "contract_with_dev_messages.vy", "erc20.vy", "use_iface.vy", + "optimize_codesize.vy", "use_iface2.vy", + "contract_no_pragma.vy", # no pragma should compile with latest version + "empty.vy", # empty file still compiles with latest version ] - # Add the 0.3.9 contracts. + # Add the 0.3.10 contracts. for template in TEMPLATES: - expected.append(f"{template}_039.vy") + expected.append(f"{template}_0310.vy") names = [x.name for x in actual[VERSION_FROM_PRAGMA]] failures = [] @@ -263,7 +269,7 @@ def line(cont: str) -> int: if nonpayable_checks: assert len(nonpayable_checks) >= 1 else: - # NOTE: Vyper 0.3.10rc3 doesn't have these anymore. + # NOTE: Vyper 0.3.10 doesn't have these anymore. # But they do have a new error type instead. checks = _all(RuntimeErrorType.INVALID_CALLDATA_OR_VALUE) assert len(checks) >= 1 @@ -322,7 +328,7 @@ def test_enrich_error_int_overflow(geth_provider, traceback_contract, account): def test_enrich_error_non_payable_check(geth_provider, traceback_contract, account): - if traceback_contract.contract_type.name.endswith("0310rc3"): + if traceback_contract.contract_type.name.endswith("0310"): # NOTE: Nonpayable error is combined with calldata check now. with pytest.raises(InvalidCalldataOrValueError): traceback_contract.addBalance(123, sender=account, value=1) From 13d94c934c0d2d214ad9b877a4c39903ddfe63d3 Mon Sep 17 00:00:00 2001 From: z80 Date: Tue, 24 Oct 2023 13:55:29 -0400 Subject: [PATCH 08/16] add info to README --- README.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b3fb5866..ad17f596 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,25 @@ vyper: Import the voting contract types like this: ```python -# @version 0.3.9 +# @version 0.3.10 import voting.ballot as ballot ``` + + +### Pragmas + +Ape-Vyper supports Vyper 0.3.10's [new pragma formats](https://github.com/vyperlang/vyper/pull/3493) + +#### Version Pragma + +``` python +#pragma version 0.3.10 +``` + +#### Optimization Pragma + +``` python +#pragma optimize codesize +``` + From fe56b937480a8d399bb1af241b76f9cfe0549da4 Mon Sep 17 00:00:00 2001 From: z80 Date: Tue, 24 Oct 2023 14:11:02 -0400 Subject: [PATCH 09/16] fix: remove pprint --- tests/test_compiler.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/test_compiler.py b/tests/test_compiler.py index 93b8afbf..433ae840 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -1,5 +1,4 @@ import re -import pprint import pytest from ape.exceptions import ContractLogicError @@ -81,9 +80,7 @@ def test_get_version_map(project, compiler, all_versions): x for x in project.contracts_folder.iterdir() if x.is_file() and x.suffix == ".vy" ] actual = compiler.get_version_map(vyper_files) - pprint.pprint(actual) expected_versions = [Version(v) for v in all_versions] - pprint.pprint(expected_versions) for version, sources in actual.items(): if version in expected_versions: @@ -103,8 +100,8 @@ def test_get_version_map(project, compiler, all_versions): "use_iface.vy", "optimize_codesize.vy", "use_iface2.vy", - "contract_no_pragma.vy", # no pragma should compile with latest version - "empty.vy", # empty file still compiles with latest version + "contract_no_pragma.vy", # no pragma should compile with latest version + "empty.vy", # empty file still compiles with latest version ] # Add the 0.3.10 contracts. From c90f3f4fa73f63430fe6d26b0594ccd66c1a4a69 Mon Sep 17 00:00:00 2001 From: z80 Date: Tue, 24 Oct 2023 14:20:26 -0400 Subject: [PATCH 10/16] fix: mdformat --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ad17f596..bd0c6820 100644 --- a/README.md +++ b/README.md @@ -72,20 +72,18 @@ Import the voting contract types like this: import voting.ballot as ballot ``` - ### Pragmas Ape-Vyper supports Vyper 0.3.10's [new pragma formats](https://github.com/vyperlang/vyper/pull/3493) #### Version Pragma -``` python +```python #pragma version 0.3.10 ``` #### Optimization Pragma -``` python +```python #pragma optimize codesize ``` - From b13e2a1451cfac724a7b070729e30723f5a91f9f Mon Sep 17 00:00:00 2001 From: z80 Date: Tue, 24 Oct 2023 15:24:11 -0400 Subject: [PATCH 11/16] fix: update metadata test --- tests/test_compiler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_compiler.py b/tests/test_compiler.py index 433ae840..2d0b728c 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -144,7 +144,7 @@ def test_compiler_data_in_manifest(project): assert len(vyper_latest.contractTypes) >= 9 assert len(vyper_028.contractTypes) >= 1 - assert "contract_039" in vyper_latest.contractTypes + assert "contract_0310" in vyper_latest.contractTypes assert "older_version" in vyper_028.contractTypes for compiler in (vyper_latest, vyper_028): assert compiler.settings["evmVersion"] == "istanbul" From 8bd0013d87c5e8507f05dd44f5d67fa3f848db73 Mon Sep 17 00:00:00 2001 From: z80 Date: Tue, 24 Oct 2023 18:07:19 -0400 Subject: [PATCH 12/16] fix: address PR comments --- ape_vyper/compiler.py | 9 +++++---- tests/contracts/passing_contracts/pragma_with_space.vy | 7 +++++++ tests/test_compiler.py | 1 + 3 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 tests/contracts/passing_contracts/pragma_with_space.vy diff --git a/ape_vyper/compiler.py b/ape_vyper/compiler.py index 23c5c20c..169de7d6 100644 --- a/ape_vyper/compiler.py +++ b/ape_vyper/compiler.py @@ -83,7 +83,7 @@ def get_version_pragma_spec(source: Union[str, Path]) -> Optional[SpecifierSet]: if pragma_match is None: # support new pragma syntax pragma_match = next( - re.finditer(r"(?:\n|^)\s*#pragma\s+version\s*([^\n]*)", source_str), None + re.finditer(r"(?:\n|^)\s*#\s*pragma\s+version\s*([^\n]*)", source_str), None ) if pragma_match is None: return None # Try compiling with latest @@ -100,19 +100,20 @@ def get_version_pragma_spec(source: Union[str, Path]) -> Optional[SpecifierSet]: return None -def get_optimization_pragma(source: Union[str, Path]) -> Union[str, bool]: +def get_optimization_pragma(source: Union[str, Path]) -> Optional[str]: """ Extracts optimization pragma information from Vyper source code. Args: source (str): Vyper source code + Returns: ``str``, or True if no valid pragma is found (for backwards compatibility). """ source_str = source if isinstance(source, str) else source.read_text() pragma_match = next(re.finditer(r"(?:\n|^)\s*#pragma\s+optimize\s+([^\n]*)", source_str), None) if pragma_match is None: - return True + return None return pragma_match.groups()[0] @@ -307,7 +308,7 @@ def compile( for optimization, source_paths in optimizations_map.items(): settings: Dict[str, Any] = version_settings.copy() - settings["optimize"] = optimization + settings["optimize"] = optimization or True path_args = { str(get_relative_path(p.absolute(), base_path)): p for p in source_paths } diff --git a/tests/contracts/passing_contracts/pragma_with_space.vy b/tests/contracts/passing_contracts/pragma_with_space.vy new file mode 100644 index 00000000..7b69ecf8 --- /dev/null +++ b/tests/contracts/passing_contracts/pragma_with_space.vy @@ -0,0 +1,7 @@ +# pragma version 0.3.10 + +x: uint256 + +@external +def __init__(): + self.x = 0 diff --git a/tests/test_compiler.py b/tests/test_compiler.py index 2d0b728c..b4258b5f 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -102,6 +102,7 @@ def test_get_version_map(project, compiler, all_versions): "use_iface2.vy", "contract_no_pragma.vy", # no pragma should compile with latest version "empty.vy", # empty file still compiles with latest version + "pragma_with_space.vy", ] # Add the 0.3.10 contracts. From 64e29e95b4536cd7c4ff82fa36f9e775145d0a3c Mon Sep 17 00:00:00 2001 From: z80 Date: Tue, 24 Oct 2023 18:21:39 -0400 Subject: [PATCH 13/16] fix: pin pydantic --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index bf635bd5..30a487ce 100644 --- a/setup.py +++ b/setup.py @@ -14,6 +14,7 @@ "black>=23.3.0,<24", # Auto-formatter and linter "mypy>=0.991,<1", # Static type analyzer "types-setuptools", # Needed due to mypy typeshed + "pydantic<2.0", # Needed for successful type check. TODO: Remove after full v2 support. "flake8>=6.0.0,<7", # Style linter "isort>=5.10.1", # Import sorting linter "mdformat>=0.7.16", # Auto-formatter for markdown From 8df6803286b7ee0d37d27da3815f3eb471f10ff6 Mon Sep 17 00:00:00 2001 From: z80 Date: Tue, 24 Oct 2023 18:47:04 -0400 Subject: [PATCH 14/16] walrus operator was swallowing stuff --- ape_vyper/compiler.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ape_vyper/compiler.py b/ape_vyper/compiler.py index 169de7d6..42cc2e85 100644 --- a/ape_vyper/compiler.py +++ b/ape_vyper/compiler.py @@ -402,10 +402,10 @@ def get_optimization_pragma_map( base_path = base_path or self.config_manager.contracts_folder optimization_pragma_map: Dict[Union[str, bool], Set[Path]] = {} for path in contract_filepaths: - if pragma := get_optimization_pragma(path): - if pragma not in optimization_pragma_map: - optimization_pragma_map[pragma] = set() - optimization_pragma_map[pragma].add(path) + pragma = get_optimization_pragma(path) or True + if pragma not in optimization_pragma_map: + optimization_pragma_map[pragma] = set() + optimization_pragma_map[pragma].add(path) return optimization_pragma_map From af57d27fb79e9e88906c8a0aef8b78e3372b7bc1 Mon Sep 17 00:00:00 2001 From: z80 Date: Wed, 25 Oct 2023 13:01:10 -0400 Subject: [PATCH 15/16] fix: make version pragma check more readable --- ape_vyper/compiler.py | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/ape_vyper/compiler.py b/ape_vyper/compiler.py index 42cc2e85..e1ab6b8d 100644 --- a/ape_vyper/compiler.py +++ b/ape_vyper/compiler.py @@ -78,26 +78,25 @@ def get_version_pragma_spec(source: Union[str, Path]) -> Optional[SpecifierSet]: Returns: ``packaging.specifiers.SpecifierSet``, or None if no valid pragma is found. """ - source_str = source if isinstance(source, str) else source.read_text() - pragma_match = next(re.finditer(r"(?:\n|^)\s*#\s*@version\s*([^\n]*)", source_str), None) - if pragma_match is None: - # support new pragma syntax - pragma_match = next( - re.finditer(r"(?:\n|^)\s*#\s*pragma\s+version\s*([^\n]*)", source_str), None - ) - if pragma_match is None: - return None # Try compiling with latest + version_pragma_patterns = [ + r"(?:\n|^)\s*#\s*@version\s*([^\n]*)", + r"(?:\n|^)\s*#\s*pragma\s+version\s*([^\n]*)", + ] - raw_pragma = pragma_match.groups()[0] - pragma_str = " ".join(raw_pragma.split()).replace("^", "~=") - if pragma_str and pragma_str[0].isnumeric(): - pragma_str = f"=={pragma_str}" + source_str = source if isinstance(source, str) else source.read_text() + for pattern in version_pragma_patterns: + for match in re.finditer(pattern, source_str): + raw_pragma = match.groups()[0] + pragma_str = " ".join(raw_pragma.split()).replace("^", "~=") + if pragma_str and pragma_str[0].isnumeric(): + pragma_str = f"=={pragma_str}" - try: - return SpecifierSet(pragma_str) - except InvalidSpecifier: - logger.warning(f"Invalid pragma spec: '{raw_pragma}'. Trying latest.") - return None + try: + return SpecifierSet(pragma_str) + except InvalidSpecifier: + logger.warning(f"Invalid pragma spec: '{raw_pragma}'. Trying latest.") + return None + return None def get_optimization_pragma(source: Union[str, Path]) -> Optional[str]: From 2911cc13aebc6b106e460ac8d5a81368a8fb9b13 Mon Sep 17 00:00:00 2001 From: z80 Date: Thu, 26 Oct 2023 10:30:00 -0400 Subject: [PATCH 16/16] fix: address PR feedback --- ape_vyper/compiler.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/ape_vyper/compiler.py b/ape_vyper/compiler.py index e1ab6b8d..411fe195 100644 --- a/ape_vyper/compiler.py +++ b/ape_vyper/compiler.py @@ -104,10 +104,10 @@ def get_optimization_pragma(source: Union[str, Path]) -> Optional[str]: Extracts optimization pragma information from Vyper source code. Args: - source (str): Vyper source code + source (Union[str, Path]): Vyper source code Returns: - ``str``, or True if no valid pragma is found (for backwards compatibility). + ``str``, or None if no valid pragma is found. """ source_str = source if isinstance(source, str) else source.read_text() pragma_match = next(re.finditer(r"(?:\n|^)\s*#pragma\s+optimize\s+([^\n]*)", source_str), None) @@ -291,6 +291,13 @@ def import_remapping(self) -> Dict[str, Dict]: return interfaces + def classify_ast(self, _node: ASTNode): + if _node.ast_type in _FUNCTION_AST_TYPES: + _node.classification = ASTClassification.FUNCTION + + for child in _node.children: + self.classify_ast(child) + def compile( self, contract_filepaths: List[Path], base_path: Optional[Path] = None ) -> List[ContractType]: @@ -332,13 +339,6 @@ def compile( except VyperError as err: raise VyperCompileError(err) from err - def classify_ast(_node: ASTNode): - if _node.ast_type in _FUNCTION_AST_TYPES: - _node.classification = ASTClassification.FUNCTION - - for child in _node.children: - classify_ast(child) - for source_id, output_items in result["contracts"].items(): content = { i + 1: ln @@ -347,7 +347,7 @@ def classify_ast(_node: ASTNode): for name, output in output_items.items(): # De-compress source map to get PC POS map. ast = ASTNode.parse_obj(result["sources"][source_id]["ast"]) - classify_ast(ast) + self.classify_ast(ast) # Track function offsets. function_offsets = []