diff --git a/tests/base_conftest.py b/tests/base_conftest.py index 1c7c6f3aed..f613ad0f47 100644 --- a/tests/base_conftest.py +++ b/tests/base_conftest.py @@ -112,16 +112,18 @@ def w3(tester): return w3 -def _get_contract(w3, source_code, optimize, *args, override_opt_level=None, **kwargs): +def _get_contract( + w3, source_code, optimize, *args, override_opt_level=None, input_bundle=None, **kwargs +): settings = Settings() settings.evm_version = kwargs.pop("evm_version", None) settings.optimize = override_opt_level or optimize out = compiler.compile_code( source_code, # test that metadata and natspecs get generated - ["abi", "bytecode", "metadata", "userdoc", "devdoc"], + output_formats=["abi", "bytecode", "metadata", "userdoc", "devdoc"], settings=settings, - interface_codes=kwargs.pop("interface_codes", None), + input_bundle=input_bundle, show_gas_estimates=True, # Enable gas estimates for testing ) parse_vyper_source(source_code) # Test grammar. @@ -144,8 +146,7 @@ def _deploy_blueprint_for(w3, source_code, optimize, initcode_prefix=b"", **kwar settings.optimize = optimize out = compiler.compile_code( source_code, - ["abi", "bytecode"], - interface_codes=kwargs.pop("interface_codes", None), + output_formats=["abi", "bytecode", "metadata", "userdoc", "devdoc"], settings=settings, show_gas_estimates=True, # Enable gas estimates for testing ) @@ -187,10 +188,10 @@ def deploy_blueprint_for(source_code, *args, **kwargs): @pytest.fixture(scope="module") def get_contract(w3, optimize): - def get_contract(source_code, *args, **kwargs): + def fn(source_code, *args, **kwargs): return _get_contract(w3, source_code, optimize, *args, **kwargs) - return get_contract + return fn @pytest.fixture diff --git a/tests/cli/vyper_compile/test_compile_files.py b/tests/cli/vyper_compile/test_compile_files.py index 31cf622658..2a16efa777 100644 --- a/tests/cli/vyper_compile/test_compile_files.py +++ b/tests/cli/vyper_compile/test_compile_files.py @@ -1,12 +1,12 @@ +from pathlib import Path + import pytest from vyper.cli.vyper_compile import compile_files -def test_combined_json_keys(tmp_path): - bar_path = tmp_path.joinpath("bar.vy") - with bar_path.open("w") as fp: - fp.write("") +def test_combined_json_keys(tmp_path, make_file): + make_file("bar.vy", "") combined_keys = { "bytecode", @@ -19,12 +19,203 @@ def test_combined_json_keys(tmp_path): "userdoc", "devdoc", } - compile_data = compile_files([bar_path], ["combined_json"], root_folder=tmp_path) + compile_data = compile_files(["bar.vy"], ["combined_json"], root_folder=tmp_path) - assert set(compile_data.keys()) == {"bar.vy", "version"} - assert set(compile_data["bar.vy"].keys()) == combined_keys + assert set(compile_data.keys()) == {Path("bar.vy"), "version"} + assert set(compile_data[Path("bar.vy")].keys()) == combined_keys def test_invalid_root_path(): with pytest.raises(FileNotFoundError): compile_files([], [], root_folder="path/that/does/not/exist") + + +FOO_CODE = """ +{} + +struct FooStruct: + foo_: uint256 + +@external +def foo() -> FooStruct: + return FooStruct({{foo_: 13}}) + +@external +def bar(a: address) -> FooStruct: + return {}(a).bar() +""" + +BAR_CODE = """ +struct FooStruct: + foo_: uint256 +@external +def bar() -> FooStruct: + return FooStruct({foo_: 13}) +""" + + +SAME_FOLDER_IMPORT_STMT = [ + ("import Bar as Bar", "Bar"), + ("import contracts.Bar as Bar", "Bar"), + ("from . import Bar", "Bar"), + ("from contracts import Bar", "Bar"), + ("from ..contracts import Bar", "Bar"), + ("from . import Bar as FooBar", "FooBar"), + ("from contracts import Bar as FooBar", "FooBar"), + ("from ..contracts import Bar as FooBar", "FooBar"), +] + + +@pytest.mark.parametrize("import_stmt,alias", SAME_FOLDER_IMPORT_STMT) +def test_import_same_folder(import_stmt, alias, tmp_path, make_file): + foo = "contracts/foo.vy" + make_file("contracts/foo.vy", FOO_CODE.format(import_stmt, alias)) + make_file("contracts/Bar.vy", BAR_CODE) + + assert compile_files([foo], ["combined_json"], root_folder=tmp_path) + + +SUBFOLDER_IMPORT_STMT = [ + ("import other.Bar as Bar", "Bar"), + ("import contracts.other.Bar as Bar", "Bar"), + ("from other import Bar", "Bar"), + ("from contracts.other import Bar", "Bar"), + ("from .other import Bar", "Bar"), + ("from ..contracts.other import Bar", "Bar"), + ("from other import Bar as FooBar", "FooBar"), + ("from contracts.other import Bar as FooBar", "FooBar"), + ("from .other import Bar as FooBar", "FooBar"), + ("from ..contracts.other import Bar as FooBar", "FooBar"), +] + + +@pytest.mark.parametrize("import_stmt, alias", SUBFOLDER_IMPORT_STMT) +def test_import_subfolder(import_stmt, alias, tmp_path, make_file): + foo = make_file("contracts/foo.vy", (FOO_CODE.format(import_stmt, alias))) + make_file("contracts/other/Bar.vy", BAR_CODE) + + assert compile_files([foo], ["combined_json"], root_folder=tmp_path) + + +OTHER_FOLDER_IMPORT_STMT = [ + ("import interfaces.Bar as Bar", "Bar"), + ("from interfaces import Bar", "Bar"), + ("from ..interfaces import Bar", "Bar"), + ("from interfaces import Bar as FooBar", "FooBar"), + ("from ..interfaces import Bar as FooBar", "FooBar"), +] + + +@pytest.mark.parametrize("import_stmt, alias", OTHER_FOLDER_IMPORT_STMT) +def test_import_other_folder(import_stmt, alias, tmp_path, make_file): + foo = make_file("contracts/foo.vy", FOO_CODE.format(import_stmt, alias)) + make_file("interfaces/Bar.vy", BAR_CODE) + + assert compile_files([foo], ["combined_json"], root_folder=tmp_path) + + +def test_import_parent_folder(tmp_path, make_file): + foo = make_file("contracts/baz/foo.vy", FOO_CODE.format("from ... import Bar", "Bar")) + make_file("Bar.vy", BAR_CODE) + + assert compile_files([foo], ["combined_json"], root_folder=tmp_path) + + # perform relative import outside of base folder + compile_files([foo], ["combined_json"], root_folder=tmp_path / "contracts") + + +META_IMPORT_STMT = [ + "import Meta as Meta", + "import contracts.Meta as Meta", + "from . import Meta", + "from contracts import Meta", +] + + +@pytest.mark.parametrize("import_stmt", META_IMPORT_STMT) +def test_import_self_interface(import_stmt, tmp_path, make_file): + # a contract can access its derived interface by importing itself + code = f""" +{import_stmt} + +struct FooStruct: + foo_: uint256 + +@external +def know_thyself(a: address) -> FooStruct: + return Meta(a).be_known() + +@external +def be_known() -> FooStruct: + return FooStruct({{foo_: 42}}) + """ + meta = make_file("contracts/Meta.vy", code) + + assert compile_files([meta], ["combined_json"], root_folder=tmp_path) + + +DERIVED_IMPORT_STMT_BAZ = ["import Foo as Foo", "from . import Foo"] + +DERIVED_IMPORT_STMT_FOO = ["import Bar as Bar", "from . import Bar"] + + +@pytest.mark.parametrize("import_stmt_baz", DERIVED_IMPORT_STMT_BAZ) +@pytest.mark.parametrize("import_stmt_foo", DERIVED_IMPORT_STMT_FOO) +def test_derived_interface_imports(import_stmt_baz, import_stmt_foo, tmp_path, make_file): + # contracts-as-interfaces should be able to contain import statements + baz_code = f""" +{import_stmt_baz} + +struct FooStruct: + foo_: uint256 + +@external +def foo(a: address) -> FooStruct: + return Foo(a).foo() + +@external +def bar(_foo: address, _bar: address) -> FooStruct: + return Foo(_foo).bar(_bar) + """ + + make_file("Foo.vy", FOO_CODE.format(import_stmt_foo, "Bar")) + make_file("Bar.vy", BAR_CODE) + baz = make_file("Baz.vy", baz_code) + + assert compile_files([baz], ["combined_json"], root_folder=tmp_path) + + +def test_local_namespace(make_file, tmp_path): + # interface code namespaces should be isolated + # all of these contract should be able to compile together + codes = [ + "import foo as FooBar", + "import bar as FooBar", + "import foo as BarFoo", + "import bar as BarFoo", + ] + struct_def = """ +struct FooStruct: + foo_: uint256 + + """ + + paths = [] + for i, code in enumerate(codes): + code += struct_def + filename = f"code{i}.vy" + make_file(filename, code) + paths.append(filename) + + for file_name in ("foo.vy", "bar.vy"): + make_file(file_name, BAR_CODE) + + assert compile_files(paths, ["combined_json"], root_folder=tmp_path) + + +def test_compile_outside_root_path(tmp_path, make_file): + # absolute paths relative to "." + foo = make_file("foo.vy", FOO_CODE.format("import bar as Bar", "Bar")) + bar = make_file("bar.vy", BAR_CODE) + + assert compile_files([foo, bar], ["combined_json"], root_folder=".") diff --git a/tests/cli/vyper_compile/test_import_paths.py b/tests/cli/vyper_compile/test_import_paths.py deleted file mode 100644 index 81f209113f..0000000000 --- a/tests/cli/vyper_compile/test_import_paths.py +++ /dev/null @@ -1,260 +0,0 @@ -import pytest - -from vyper.cli.vyper_compile import compile_files, get_interface_file_path - -FOO_CODE = """ -{} - -struct FooStruct: - foo_: uint256 - -@external -def foo() -> FooStruct: - return FooStruct({{foo_: 13}}) - -@external -def bar(a: address) -> FooStruct: - return {}(a).bar() -""" - -BAR_CODE = """ -struct FooStruct: - foo_: uint256 -@external -def bar() -> FooStruct: - return FooStruct({foo_: 13}) -""" - - -SAME_FOLDER_IMPORT_STMT = [ - ("import Bar as Bar", "Bar"), - ("import contracts.Bar as Bar", "Bar"), - ("from . import Bar", "Bar"), - ("from contracts import Bar", "Bar"), - ("from ..contracts import Bar", "Bar"), - ("from . import Bar as FooBar", "FooBar"), - ("from contracts import Bar as FooBar", "FooBar"), - ("from ..contracts import Bar as FooBar", "FooBar"), -] - - -@pytest.mark.parametrize("import_stmt,alias", SAME_FOLDER_IMPORT_STMT) -def test_import_same_folder(import_stmt, alias, tmp_path): - tmp_path.joinpath("contracts").mkdir() - - foo_path = tmp_path.joinpath("contracts/foo.vy") - with foo_path.open("w") as fp: - fp.write(FOO_CODE.format(import_stmt, alias)) - - with tmp_path.joinpath("contracts/Bar.vy").open("w") as fp: - fp.write(BAR_CODE) - - assert compile_files([foo_path], ["combined_json"], root_folder=tmp_path) - - -SUBFOLDER_IMPORT_STMT = [ - ("import other.Bar as Bar", "Bar"), - ("import contracts.other.Bar as Bar", "Bar"), - ("from other import Bar", "Bar"), - ("from contracts.other import Bar", "Bar"), - ("from .other import Bar", "Bar"), - ("from ..contracts.other import Bar", "Bar"), - ("from other import Bar as FooBar", "FooBar"), - ("from contracts.other import Bar as FooBar", "FooBar"), - ("from .other import Bar as FooBar", "FooBar"), - ("from ..contracts.other import Bar as FooBar", "FooBar"), -] - - -@pytest.mark.parametrize("import_stmt, alias", SUBFOLDER_IMPORT_STMT) -def test_import_subfolder(import_stmt, alias, tmp_path): - tmp_path.joinpath("contracts").mkdir() - - foo_path = tmp_path.joinpath("contracts/foo.vy") - with foo_path.open("w") as fp: - fp.write(FOO_CODE.format(import_stmt, alias)) - - tmp_path.joinpath("contracts/other").mkdir() - with tmp_path.joinpath("contracts/other/Bar.vy").open("w") as fp: - fp.write(BAR_CODE) - - assert compile_files([foo_path], ["combined_json"], root_folder=tmp_path) - - -OTHER_FOLDER_IMPORT_STMT = [ - ("import interfaces.Bar as Bar", "Bar"), - ("from interfaces import Bar", "Bar"), - ("from ..interfaces import Bar", "Bar"), - ("from interfaces import Bar as FooBar", "FooBar"), - ("from ..interfaces import Bar as FooBar", "FooBar"), -] - - -@pytest.mark.parametrize("import_stmt, alias", OTHER_FOLDER_IMPORT_STMT) -def test_import_other_folder(import_stmt, alias, tmp_path): - tmp_path.joinpath("contracts").mkdir() - - foo_path = tmp_path.joinpath("contracts/foo.vy") - with foo_path.open("w") as fp: - fp.write(FOO_CODE.format(import_stmt, alias)) - - tmp_path.joinpath("interfaces").mkdir() - with tmp_path.joinpath("interfaces/Bar.vy").open("w") as fp: - fp.write(BAR_CODE) - - assert compile_files([foo_path], ["combined_json"], root_folder=tmp_path) - - -def test_import_parent_folder(tmp_path, assert_compile_failed): - tmp_path.joinpath("contracts").mkdir() - tmp_path.joinpath("contracts/baz").mkdir() - - foo_path = tmp_path.joinpath("contracts/baz/foo.vy") - with foo_path.open("w") as fp: - fp.write(FOO_CODE.format("from ... import Bar", "Bar")) - - with tmp_path.joinpath("Bar.vy").open("w") as fp: - fp.write(BAR_CODE) - - assert compile_files([foo_path], ["combined_json"], root_folder=tmp_path) - # Cannot perform relative import outside of base folder - with pytest.raises(FileNotFoundError): - compile_files([foo_path], ["combined_json"], root_folder=tmp_path.joinpath("contracts")) - - -META_IMPORT_STMT = [ - "import Meta as Meta", - "import contracts.Meta as Meta", - "from . import Meta", - "from contracts import Meta", -] - - -@pytest.mark.parametrize("import_stmt", META_IMPORT_STMT) -def test_import_self_interface(import_stmt, tmp_path): - # a contract can access its derived interface by importing itself - code = f""" -{import_stmt} - -struct FooStruct: - foo_: uint256 - -@external -def know_thyself(a: address) -> FooStruct: - return Meta(a).be_known() - -@external -def be_known() -> FooStruct: - return FooStruct({{foo_: 42}}) - """ - - tmp_path.joinpath("contracts").mkdir() - - meta_path = tmp_path.joinpath("contracts/Meta.vy") - with meta_path.open("w") as fp: - fp.write(code) - - assert compile_files([meta_path], ["combined_json"], root_folder=tmp_path) - - -DERIVED_IMPORT_STMT_BAZ = ["import Foo as Foo", "from . import Foo"] - -DERIVED_IMPORT_STMT_FOO = ["import Bar as Bar", "from . import Bar"] - - -@pytest.mark.parametrize("import_stmt_baz", DERIVED_IMPORT_STMT_BAZ) -@pytest.mark.parametrize("import_stmt_foo", DERIVED_IMPORT_STMT_FOO) -def test_derived_interface_imports(import_stmt_baz, import_stmt_foo, tmp_path): - # contracts-as-interfaces should be able to contain import statements - baz_code = f""" -{import_stmt_baz} - -struct FooStruct: - foo_: uint256 - -@external -def foo(a: address) -> FooStruct: - return Foo(a).foo() - -@external -def bar(_foo: address, _bar: address) -> FooStruct: - return Foo(_foo).bar(_bar) - """ - - with tmp_path.joinpath("Foo.vy").open("w") as fp: - fp.write(FOO_CODE.format(import_stmt_foo, "Bar")) - - with tmp_path.joinpath("Bar.vy").open("w") as fp: - fp.write(BAR_CODE) - - baz_path = tmp_path.joinpath("Baz.vy") - with baz_path.open("w") as fp: - fp.write(baz_code) - - assert compile_files([baz_path], ["combined_json"], root_folder=tmp_path) - - -def test_local_namespace(tmp_path): - # interface code namespaces should be isolated - # all of these contract should be able to compile together - codes = [ - "import foo as FooBar", - "import bar as FooBar", - "import foo as BarFoo", - "import bar as BarFoo", - ] - struct_def = """ -struct FooStruct: - foo_: uint256 - - """ - - compile_paths = [] - for i, code in enumerate(codes): - code += struct_def - path = tmp_path.joinpath(f"code{i}.vy") - with path.open("w") as fp: - fp.write(code) - compile_paths.append(path) - - for file_name in ("foo.vy", "bar.vy"): - with tmp_path.joinpath(file_name).open("w") as fp: - fp.write(BAR_CODE) - - assert compile_files(compile_paths, ["combined_json"], root_folder=tmp_path) - - -def test_get_interface_file_path(tmp_path): - for file_name in ("foo.vy", "foo.json", "bar.vy", "baz.json", "potato"): - with tmp_path.joinpath(file_name).open("w") as fp: - fp.write("") - - tmp_path.joinpath("interfaces").mkdir() - for file_name in ("interfaces/foo.json", "interfaces/bar"): - with tmp_path.joinpath(file_name).open("w") as fp: - fp.write("") - - base_paths = [tmp_path, tmp_path.joinpath("interfaces")] - assert get_interface_file_path(base_paths, "foo") == tmp_path.joinpath("foo.vy") - assert get_interface_file_path(base_paths, "bar") == tmp_path.joinpath("bar.vy") - assert get_interface_file_path(base_paths, "baz") == tmp_path.joinpath("baz.json") - - base_paths = [tmp_path.joinpath("interfaces"), tmp_path] - assert get_interface_file_path(base_paths, "foo") == tmp_path.joinpath("interfaces/foo.json") - assert get_interface_file_path(base_paths, "bar") == tmp_path.joinpath("bar.vy") - assert get_interface_file_path(base_paths, "baz") == tmp_path.joinpath("baz.json") - - with pytest.raises(Exception): - get_interface_file_path(base_paths, "potato") - - -def test_compile_outside_root_path(tmp_path): - foo_path = tmp_path.joinpath("foo.vy") - with foo_path.open("w") as fp: - fp.write(FOO_CODE.format("import bar as Bar", "Bar")) - - bar_path = tmp_path.joinpath("bar.vy") - with bar_path.open("w") as fp: - fp.write(BAR_CODE) - - assert compile_files([foo_path, bar_path], ["combined_json"], root_folder=".") diff --git a/tests/cli/vyper_compile/test_parse_args.py b/tests/cli/vyper_compile/test_parse_args.py index a676a7836b..0e8c4e9605 100644 --- a/tests/cli/vyper_compile/test_parse_args.py +++ b/tests/cli/vyper_compile/test_parse_args.py @@ -21,7 +21,9 @@ def foo() -> bool: bar_path = chdir_path.joinpath("bar.vy") with bar_path.open("w") as fp: fp.write(code) + _parse_args([str(bar_path)]) # absolute path os.chdir(chdir_path.parent) + _parse_args([str(bar_path)]) # absolute path, subfolder of cwd _parse_args([str(bar_path.relative_to(chdir_path.parent))]) # relative path diff --git a/tests/cli/vyper_json/test_compile_from_input_dict.py b/tests/cli/vyper_json/test_compile_from_input_dict.py deleted file mode 100644 index a6d0a23100..0000000000 --- a/tests/cli/vyper_json/test_compile_from_input_dict.py +++ /dev/null @@ -1,132 +0,0 @@ -#!/usr/bin/env python3 - -from copy import deepcopy - -import pytest - -import vyper -from vyper.cli.vyper_json import ( - TRANSLATE_MAP, - compile_from_input_dict, - exc_handler_raises, - exc_handler_to_dict, -) -from vyper.exceptions import InvalidType, JSONError, SyntaxException - -FOO_CODE = """ -import contracts.bar as Bar - -@external -def foo(a: address) -> bool: - return Bar(a).bar(1) - -@external -def baz() -> uint256: - return self.balance -""" - -BAR_CODE = """ -@external -def bar(a: uint256) -> bool: - return True -""" - -BAD_SYNTAX_CODE = """ -def bar()>: -""" - -BAD_COMPILER_CODE = """ -@external -def oopsie(a: uint256) -> bool: - return 42 -""" - -BAR_ABI = [ - { - "name": "bar", - "outputs": [{"type": "bool", "name": "out"}], - "inputs": [{"type": "uint256", "name": "a"}], - "stateMutability": "nonpayable", - "type": "function", - "gas": 313, - } -] - -INPUT_JSON = { - "language": "Vyper", - "sources": { - "contracts/foo.vy": {"content": FOO_CODE}, - "contracts/bar.vy": {"content": BAR_CODE}, - }, - "interfaces": {"contracts/bar.json": {"abi": BAR_ABI}}, - "settings": {"outputSelection": {"*": ["*"]}}, -} - - -def test_root_folder_not_exists(): - with pytest.raises(FileNotFoundError): - compile_from_input_dict({}, root_folder="/path/that/does/not/exist") - - -def test_wrong_language(): - with pytest.raises(JSONError): - compile_from_input_dict({"language": "Solidity"}) - - -def test_exc_handler_raises_syntax(): - input_json = deepcopy(INPUT_JSON) - input_json["sources"]["badcode.vy"] = {"content": BAD_SYNTAX_CODE} - with pytest.raises(SyntaxException): - compile_from_input_dict(input_json, exc_handler_raises) - - -def test_exc_handler_to_dict_syntax(): - input_json = deepcopy(INPUT_JSON) - input_json["sources"]["badcode.vy"] = {"content": BAD_SYNTAX_CODE} - result, _ = compile_from_input_dict(input_json, exc_handler_to_dict) - assert "errors" in result - assert len(result["errors"]) == 1 - error = result["errors"][0] - assert error["component"] == "parser" - assert error["type"] == "SyntaxException" - - -def test_exc_handler_raises_compiler(): - input_json = deepcopy(INPUT_JSON) - input_json["sources"]["badcode.vy"] = {"content": BAD_COMPILER_CODE} - with pytest.raises(InvalidType): - compile_from_input_dict(input_json, exc_handler_raises) - - -def test_exc_handler_to_dict_compiler(): - input_json = deepcopy(INPUT_JSON) - input_json["sources"]["badcode.vy"] = {"content": BAD_COMPILER_CODE} - result, _ = compile_from_input_dict(input_json, exc_handler_to_dict) - assert sorted(result.keys()) == ["compiler", "errors"] - assert result["compiler"] == f"vyper-{vyper.__version__}" - assert len(result["errors"]) == 1 - error = result["errors"][0] - assert error["component"] == "compiler" - assert error["type"] == "InvalidType" - - -def test_source_ids_increment(): - input_json = deepcopy(INPUT_JSON) - input_json["settings"]["outputSelection"] = {"*": ["evm.deployedBytecode.sourceMap"]} - result, _ = compile_from_input_dict(input_json) - assert result["contracts/bar.vy"]["source_map"]["pc_pos_map_compressed"].startswith("-1:-1:0") - assert result["contracts/foo.vy"]["source_map"]["pc_pos_map_compressed"].startswith("-1:-1:1") - - -def test_outputs(): - result, _ = compile_from_input_dict(INPUT_JSON) - assert sorted(result.keys()) == ["contracts/bar.vy", "contracts/foo.vy"] - assert sorted(result["contracts/bar.vy"].keys()) == sorted(set(TRANSLATE_MAP.values())) - - -def test_relative_import_paths(): - input_json = deepcopy(INPUT_JSON) - input_json["sources"]["contracts/potato/baz/baz.vy"] = {"content": """from ... import foo"""} - input_json["sources"]["contracts/potato/baz/potato.vy"] = {"content": """from . import baz"""} - input_json["sources"]["contracts/potato/footato.vy"] = {"content": """from baz import baz"""} - compile_from_input_dict(input_json) diff --git a/tests/cli/vyper_json/test_compile_json.py b/tests/cli/vyper_json/test_compile_json.py index f03006c4ad..732762d72b 100644 --- a/tests/cli/vyper_json/test_compile_json.py +++ b/tests/cli/vyper_json/test_compile_json.py @@ -1,12 +1,11 @@ -#!/usr/bin/env python3 - import json -from copy import deepcopy import pytest -from vyper.cli.vyper_json import compile_from_input_dict, compile_json -from vyper.exceptions import JSONError +import vyper +from vyper.cli.vyper_json import compile_from_input_dict, compile_json, exc_handler_to_dict +from vyper.compiler import OUTPUT_FORMATS, compile_code +from vyper.exceptions import InvalidType, JSONError, SyntaxException FOO_CODE = """ import contracts.bar as Bar @@ -14,6 +13,10 @@ @external def foo(a: address) -> bool: return Bar(a).bar(1) + +@external +def baz() -> uint256: + return self.balance """ BAR_CODE = """ @@ -22,6 +25,16 @@ def bar(a: uint256) -> bool: return True """ +BAD_SYNTAX_CODE = """ +def bar()>: +""" + +BAD_COMPILER_CODE = """ +@external +def oopsie(a: uint256) -> bool: + return 42 +""" + BAR_ABI = [ { "name": "bar", @@ -29,23 +42,26 @@ def bar(a: uint256) -> bool: "inputs": [{"type": "uint256", "name": "a"}], "stateMutability": "nonpayable", "type": "function", - "gas": 313, } ] -INPUT_JSON = { - "language": "Vyper", - "sources": { - "contracts/foo.vy": {"content": FOO_CODE}, - "contracts/bar.vy": {"content": BAR_CODE}, - }, - "interfaces": {"contracts/bar.json": {"abi": BAR_ABI}}, - "settings": {"outputSelection": {"*": ["*"]}}, -} + +@pytest.fixture(scope="function") +def input_json(): + return { + "language": "Vyper", + "sources": { + "contracts/foo.vy": {"content": FOO_CODE}, + "contracts/bar.vy": {"content": BAR_CODE}, + }, + "interfaces": {"contracts/ibar.json": {"abi": BAR_ABI}}, + "settings": {"outputSelection": {"*": ["*"]}}, + } -def test_input_formats(): - assert compile_json(INPUT_JSON) == compile_json(json.dumps(INPUT_JSON)) +# test string and dict inputs both work +def test_string_input(input_json): + assert compile_json(input_json) == compile_json(json.dumps(input_json)) def test_bad_json(): @@ -53,10 +69,146 @@ def test_bad_json(): compile_json("this probably isn't valid JSON, is it") -def test_keyerror_becomes_jsonerror(): - input_json = deepcopy(INPUT_JSON) +def test_keyerror_becomes_jsonerror(input_json): del input_json["sources"] with pytest.raises(KeyError): compile_from_input_dict(input_json) with pytest.raises(JSONError): compile_json(input_json) + + +def test_compile_json(input_json, make_input_bundle): + input_bundle = make_input_bundle({"contracts/bar.vy": BAR_CODE}) + + foo = compile_code( + FOO_CODE, + source_id=0, + contract_name="contracts/foo.vy", + output_formats=OUTPUT_FORMATS, + input_bundle=input_bundle, + ) + bar = compile_code( + BAR_CODE, source_id=1, contract_name="contracts/bar.vy", output_formats=OUTPUT_FORMATS + ) + + compile_code_results = {"contracts/bar.vy": bar, "contracts/foo.vy": foo} + + output_json = compile_json(input_json) + assert list(output_json["contracts"].keys()) == ["contracts/foo.vy", "contracts/bar.vy"] + + assert sorted(output_json.keys()) == ["compiler", "contracts", "sources"] + assert output_json["compiler"] == f"vyper-{vyper.__version__}" + + for source_id, contract_name in enumerate(["foo", "bar"]): + path = f"contracts/{contract_name}.vy" + data = compile_code_results[path] + assert output_json["sources"][path] == {"id": source_id, "ast": data["ast_dict"]["ast"]} + assert output_json["contracts"][path][contract_name] == { + "abi": data["abi"], + "devdoc": data["devdoc"], + "interface": data["interface"], + "ir": data["ir_dict"], + "userdoc": data["userdoc"], + "metadata": data["metadata"], + "evm": { + "bytecode": {"object": data["bytecode"], "opcodes": data["opcodes"]}, + "deployedBytecode": { + "object": data["bytecode_runtime"], + "opcodes": data["opcodes_runtime"], + "sourceMap": data["source_map"]["pc_pos_map_compressed"], + "sourceMapFull": data["source_map_full"], + }, + "methodIdentifiers": data["method_identifiers"], + }, + } + + +def test_different_outputs(make_input_bundle, input_json): + input_json["settings"]["outputSelection"] = { + "contracts/bar.vy": "*", + "contracts/foo.vy": ["evm.methodIdentifiers"], + } + output_json = compile_json(input_json) + assert list(output_json["contracts"].keys()) == ["contracts/foo.vy", "contracts/bar.vy"] + + assert sorted(output_json.keys()) == ["compiler", "contracts", "sources"] + assert output_json["compiler"] == f"vyper-{vyper.__version__}" + + contracts = output_json["contracts"] + + foo = contracts["contracts/foo.vy"]["foo"] + bar = contracts["contracts/bar.vy"]["bar"] + assert sorted(bar.keys()) == ["abi", "devdoc", "evm", "interface", "ir", "metadata", "userdoc"] + + assert sorted(foo.keys()) == ["evm"] + + # check method_identifiers + input_bundle = make_input_bundle({"contracts/bar.vy": BAR_CODE}) + method_identifiers = compile_code( + FOO_CODE, + contract_name="contracts/foo.vy", + output_formats=["method_identifiers"], + input_bundle=input_bundle, + )["method_identifiers"] + assert foo["evm"]["methodIdentifiers"] == method_identifiers + + +def test_root_folder_not_exists(input_json): + with pytest.raises(FileNotFoundError): + compile_json(input_json, root_folder="/path/that/does/not/exist") + + +def test_wrong_language(): + with pytest.raises(JSONError): + compile_json({"language": "Solidity"}) + + +def test_exc_handler_raises_syntax(input_json): + input_json["sources"]["badcode.vy"] = {"content": BAD_SYNTAX_CODE} + with pytest.raises(SyntaxException): + compile_json(input_json) + + +def test_exc_handler_to_dict_syntax(input_json): + input_json["sources"]["badcode.vy"] = {"content": BAD_SYNTAX_CODE} + result = compile_json(input_json, exc_handler_to_dict) + assert "errors" in result + assert len(result["errors"]) == 1 + error = result["errors"][0] + assert error["component"] == "compiler", error + assert error["type"] == "SyntaxException" + + +def test_exc_handler_raises_compiler(input_json): + input_json["sources"]["badcode.vy"] = {"content": BAD_COMPILER_CODE} + with pytest.raises(InvalidType): + compile_json(input_json) + + +def test_exc_handler_to_dict_compiler(input_json): + input_json["sources"]["badcode.vy"] = {"content": BAD_COMPILER_CODE} + result = compile_json(input_json, exc_handler_to_dict) + assert sorted(result.keys()) == ["compiler", "errors"] + assert result["compiler"] == f"vyper-{vyper.__version__}" + assert len(result["errors"]) == 1 + error = result["errors"][0] + assert error["component"] == "compiler" + assert error["type"] == "InvalidType" + + +def test_source_ids_increment(input_json): + input_json["settings"]["outputSelection"] = {"*": ["evm.deployedBytecode.sourceMap"]} + result = compile_json(input_json) + + def get(filename, contractname): + return result["contracts"][filename][contractname]["evm"]["deployedBytecode"]["sourceMap"] + + assert get("contracts/foo.vy", "foo").startswith("-1:-1:0") + assert get("contracts/bar.vy", "bar").startswith("-1:-1:1") + + +def test_relative_import_paths(input_json): + input_json["sources"]["contracts/potato/baz/baz.vy"] = {"content": """from ... import foo"""} + input_json["sources"]["contracts/potato/baz/potato.vy"] = {"content": """from . import baz"""} + input_json["sources"]["contracts/potato/footato.vy"] = {"content": """from baz import baz"""} + compile_from_input_dict(input_json) diff --git a/tests/cli/vyper_json/test_get_contracts.py b/tests/cli/vyper_json/test_get_contracts.py deleted file mode 100644 index 86a5052f72..0000000000 --- a/tests/cli/vyper_json/test_get_contracts.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env python3 - -import pytest - -from vyper.cli.vyper_json import get_input_dict_contracts -from vyper.exceptions import JSONError -from vyper.utils import keccak256 - -FOO_CODE = """ -import contracts.bar as Bar - -@external -def foo(a: address) -> bool: - return Bar(a).bar(1) -""" - -BAR_CODE = """ -@external -def bar(a: uint256) -> bool: - return True -""" - - -def test_no_sources(): - with pytest.raises(KeyError): - get_input_dict_contracts({}) - - -def test_contracts_urls(): - with pytest.raises(JSONError): - get_input_dict_contracts({"sources": {"foo.vy": {"urls": ["https://foo.code.com/"]}}}) - - -def test_contracts_no_content_key(): - with pytest.raises(JSONError): - get_input_dict_contracts({"sources": {"foo.vy": FOO_CODE}}) - - -def test_contracts_keccak(): - hash_ = keccak256(FOO_CODE.encode()).hex() - - input_json = {"sources": {"foo.vy": {"content": FOO_CODE, "keccak256": hash_}}} - get_input_dict_contracts(input_json) - - input_json["sources"]["foo.vy"]["keccak256"] = "0x" + hash_ - get_input_dict_contracts(input_json) - - input_json["sources"]["foo.vy"]["keccak256"] = "0x1234567890" - with pytest.raises(JSONError): - get_input_dict_contracts(input_json) - - -def test_contracts_bad_path(): - input_json = {"sources": {"../foo.vy": {"content": FOO_CODE}}} - with pytest.raises(JSONError): - get_input_dict_contracts(input_json) - - -def test_contract_collision(): - # ./foo.vy and foo.vy will resolve to the same path - input_json = {"sources": {"./foo.vy": {"content": FOO_CODE}, "foo.vy": {"content": FOO_CODE}}} - with pytest.raises(JSONError): - get_input_dict_contracts(input_json) - - -def test_contracts_return_value(): - input_json = { - "sources": {"foo.vy": {"content": FOO_CODE}, "contracts/bar.vy": {"content": BAR_CODE}} - } - result = get_input_dict_contracts(input_json) - assert result == {"foo.vy": FOO_CODE, "contracts/bar.vy": BAR_CODE} diff --git a/tests/cli/vyper_json/test_get_inputs.py b/tests/cli/vyper_json/test_get_inputs.py new file mode 100644 index 0000000000..6e323a91bd --- /dev/null +++ b/tests/cli/vyper_json/test_get_inputs.py @@ -0,0 +1,142 @@ +from pathlib import PurePath + +import pytest + +from vyper.cli.vyper_json import get_compilation_targets, get_inputs +from vyper.exceptions import JSONError +from vyper.utils import keccak256 + +FOO_CODE = """ +import contracts.bar as Bar + +@external +def foo(a: address) -> bool: + return Bar(a).bar(1) +""" + +BAR_CODE = """ +@external +def bar(a: uint256) -> bool: + return True +""" + + +def test_no_sources(): + with pytest.raises(KeyError): + get_inputs({}) + + +def test_contracts_urls(): + with pytest.raises(JSONError): + get_inputs({"sources": {"foo.vy": {"urls": ["https://foo.code.com/"]}}}) + + +def test_contracts_no_content_key(): + with pytest.raises(JSONError): + get_inputs({"sources": {"foo.vy": FOO_CODE}}) + + +def test_contracts_keccak(): + hash_ = keccak256(FOO_CODE.encode()).hex() + + input_json = {"sources": {"foo.vy": {"content": FOO_CODE, "keccak256": hash_}}} + get_inputs(input_json) + + input_json["sources"]["foo.vy"]["keccak256"] = "0x" + hash_ + get_inputs(input_json) + + input_json["sources"]["foo.vy"]["keccak256"] = "0x1234567890" + with pytest.raises(JSONError): + get_inputs(input_json) + + +def test_contracts_outside_pwd(): + input_json = {"sources": {"../foo.vy": {"content": FOO_CODE}}} + get_inputs(input_json) + + +def test_contract_collision(): + # ./foo.vy and foo.vy will resolve to the same path + input_json = {"sources": {"./foo.vy": {"content": FOO_CODE}, "foo.vy": {"content": FOO_CODE}}} + with pytest.raises(JSONError): + get_inputs(input_json) + + +def test_contracts_return_value(): + input_json = { + "sources": {"foo.vy": {"content": FOO_CODE}, "contracts/bar.vy": {"content": BAR_CODE}} + } + result = get_inputs(input_json) + assert result == { + PurePath("foo.vy"): {"content": FOO_CODE}, + PurePath("contracts/bar.vy"): {"content": BAR_CODE}, + } + + +BAR_ABI = [ + { + "name": "bar", + "outputs": [{"type": "bool", "name": "out"}], + "inputs": [{"type": "uint256", "name": "a"}], + "stateMutability": "nonpayable", + "type": "function", + } +] + + +# tests to get interfaces from input dicts + + +def test_interface_collision(): + input_json = { + "sources": {"foo.vy": {"content": FOO_CODE}}, + "interfaces": {"bar.json": {"abi": BAR_ABI}, "bar.vy": {"content": BAR_CODE}}, + } + with pytest.raises(JSONError): + get_inputs(input_json) + + +def test_json_no_abi(): + input_json = { + "sources": {"foo.vy": {"content": FOO_CODE}}, + "interfaces": {"bar.json": {"content": BAR_ABI}}, + } + with pytest.raises(JSONError): + get_inputs(input_json) + + +def test_vy_no_content(): + input_json = { + "sources": {"foo.vy": {"content": FOO_CODE}}, + "interfaces": {"bar.vy": {"abi": BAR_CODE}}, + } + with pytest.raises(JSONError): + get_inputs(input_json) + + +def test_interfaces_output(): + input_json = { + "sources": {"foo.vy": {"content": FOO_CODE}}, + "interfaces": { + "bar.json": {"abi": BAR_ABI}, + "interface.folder/bar2.vy": {"content": BAR_CODE}, + }, + } + targets = get_compilation_targets(input_json) + assert targets == [PurePath("foo.vy")] + + result = get_inputs(input_json) + assert result == { + PurePath("foo.vy"): {"content": FOO_CODE}, + PurePath("bar.json"): {"abi": BAR_ABI}, + PurePath("interface.folder/bar2.vy"): {"content": BAR_CODE}, + } + + +# EIP-2678 -- not currently supported +@pytest.mark.xfail +def test_manifest_output(): + input_json = {"interfaces": {"bar.json": {"contractTypes": {"Bar": {"abi": BAR_ABI}}}}} + result = get_inputs(input_json) + assert isinstance(result, dict) + assert result == {"Bar": {"type": "json", "code": BAR_ABI}} diff --git a/tests/cli/vyper_json/test_get_settings.py b/tests/cli/vyper_json/test_get_settings.py index bbe5dab113..989d4565cd 100644 --- a/tests/cli/vyper_json/test_get_settings.py +++ b/tests/cli/vyper_json/test_get_settings.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - import pytest from vyper.cli.vyper_json import get_evm_version diff --git a/tests/cli/vyper_json/test_interfaces.py b/tests/cli/vyper_json/test_interfaces.py deleted file mode 100644 index 7804ae1c3d..0000000000 --- a/tests/cli/vyper_json/test_interfaces.py +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/env python3 - -import pytest - -from vyper.cli.vyper_json import get_input_dict_interfaces, get_interface_codes -from vyper.exceptions import JSONError - -FOO_CODE = """ -import contracts.bar as Bar - -@external -def foo(a: address) -> bool: - return Bar(a).bar(1) -""" - -BAR_CODE = """ -@external -def bar(a: uint256) -> bool: - return True -""" - -BAR_ABI = [ - { - "name": "bar", - "outputs": [{"type": "bool", "name": "out"}], - "inputs": [{"type": "uint256", "name": "a"}], - "stateMutability": "nonpayable", - "type": "function", - "gas": 313, - } -] - - -# get_input_dict_interfaces tests - - -def test_no_interfaces(): - result = get_input_dict_interfaces({}) - assert isinstance(result, dict) - assert not result - - -def test_interface_collision(): - input_json = {"interfaces": {"bar.json": {"abi": BAR_ABI}, "bar.vy": {"content": BAR_CODE}}} - with pytest.raises(JSONError): - get_input_dict_interfaces(input_json) - - -def test_interfaces_wrong_suffix(): - input_json = {"interfaces": {"foo.abi": {"content": FOO_CODE}}} - with pytest.raises(JSONError): - get_input_dict_interfaces(input_json) - - input_json = {"interfaces": {"interface.folder/foo": {"content": FOO_CODE}}} - with pytest.raises(JSONError): - get_input_dict_interfaces(input_json) - - -def test_json_no_abi(): - input_json = {"interfaces": {"bar.json": {"content": BAR_ABI}}} - with pytest.raises(JSONError): - get_input_dict_interfaces(input_json) - - -def test_vy_no_content(): - input_json = {"interfaces": {"bar.vy": {"abi": BAR_CODE}}} - with pytest.raises(JSONError): - get_input_dict_interfaces(input_json) - - -def test_interfaces_output(): - input_json = { - "interfaces": { - "bar.json": {"abi": BAR_ABI}, - "interface.folder/bar2.vy": {"content": BAR_CODE}, - } - } - result = get_input_dict_interfaces(input_json) - assert isinstance(result, dict) - assert result == { - "bar": {"type": "json", "code": BAR_ABI}, - "interface.folder/bar2": {"type": "vyper", "code": BAR_CODE}, - } - - -def test_manifest_output(): - input_json = {"interfaces": {"bar.json": {"contractTypes": {"Bar": {"abi": BAR_ABI}}}}} - result = get_input_dict_interfaces(input_json) - assert isinstance(result, dict) - assert result == {"Bar": {"type": "json", "code": BAR_ABI}} - - -# get_interface_codes tests - - -def test_interface_codes_from_contracts(): - # interface should be generated from contract - assert get_interface_codes( - None, "foo.vy", {"foo.vy": FOO_CODE, "contracts/bar.vy": BAR_CODE}, {} - ) - assert get_interface_codes( - None, "foo/foo.vy", {"foo/foo.vy": FOO_CODE, "contracts/bar.vy": BAR_CODE}, {} - ) - - -def test_interface_codes_from_interfaces(): - # existing interface should be given preference over contract-as-interface - contracts = {"foo.vy": FOO_CODE, "contacts/bar.vy": BAR_CODE} - result = get_interface_codes(None, "foo.vy", contracts, {"contracts/bar": "bar"}) - assert result["Bar"] == "bar" - - -def test_root_path(tmp_path): - tmp_path.joinpath("contracts").mkdir() - with tmp_path.joinpath("contracts/bar.vy").open("w") as fp: - fp.write("bar") - - with pytest.raises(FileNotFoundError): - get_interface_codes(None, "foo.vy", {"foo.vy": FOO_CODE}, {}) - - # interface from file system should take lowest priority - result = get_interface_codes(tmp_path, "foo.vy", {"foo.vy": FOO_CODE}, {}) - assert result["Bar"] == {"code": "bar", "type": "vyper"} - contracts = {"foo.vy": FOO_CODE, "contracts/bar.vy": BAR_CODE} - result = get_interface_codes(None, "foo.vy", contracts, {}) - assert result["Bar"] == {"code": BAR_CODE, "type": "vyper"} diff --git a/tests/cli/vyper_json/test_output_dict.py b/tests/cli/vyper_json/test_output_dict.py deleted file mode 100644 index e2a3466ccf..0000000000 --- a/tests/cli/vyper_json/test_output_dict.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python3 - -import vyper -from vyper.cli.vyper_json import format_to_output_dict -from vyper.compiler import OUTPUT_FORMATS, compile_codes - -FOO_CODE = """ -@external -def foo() -> bool: - return True -""" - - -def test_keys(): - compiler_data = compile_codes({"foo.vy": FOO_CODE}, output_formats=list(OUTPUT_FORMATS.keys())) - output_json = format_to_output_dict(compiler_data) - assert sorted(output_json.keys()) == ["compiler", "contracts", "sources"] - assert output_json["compiler"] == f"vyper-{vyper.__version__}" - data = compiler_data["foo.vy"] - assert output_json["sources"]["foo.vy"] == {"id": 0, "ast": data["ast_dict"]["ast"]} - assert output_json["contracts"]["foo.vy"]["foo"] == { - "abi": data["abi"], - "devdoc": data["devdoc"], - "interface": data["interface"], - "ir": data["ir_dict"], - "userdoc": data["userdoc"], - "metadata": data["metadata"], - "evm": { - "bytecode": {"object": data["bytecode"], "opcodes": data["opcodes"]}, - "deployedBytecode": { - "object": data["bytecode_runtime"], - "opcodes": data["opcodes_runtime"], - "sourceMap": data["source_map"]["pc_pos_map_compressed"], - "sourceMapFull": data["source_map_full"], - }, - "methodIdentifiers": data["method_identifiers"], - }, - } diff --git a/tests/cli/vyper_json/test_output_selection.py b/tests/cli/vyper_json/test_output_selection.py index 3b12e2b54a..78ad7404f2 100644 --- a/tests/cli/vyper_json/test_output_selection.py +++ b/tests/cli/vyper_json/test_output_selection.py @@ -1,60 +1,60 @@ -#!/usr/bin/env python3 +from pathlib import PurePath import pytest -from vyper.cli.vyper_json import TRANSLATE_MAP, get_input_dict_output_formats +from vyper.cli.vyper_json import TRANSLATE_MAP, get_output_formats from vyper.exceptions import JSONError def test_no_outputs(): with pytest.raises(KeyError): - get_input_dict_output_formats({}, {}) + get_output_formats({}, {}) def test_invalid_output(): input_json = {"settings": {"outputSelection": {"foo.vy": ["abi", "foobar"]}}} - sources = {"foo.vy": ""} + targets = [PurePath("foo.vy")] with pytest.raises(JSONError): - get_input_dict_output_formats(input_json, sources) + get_output_formats(input_json, targets) def test_unknown_contract(): input_json = {"settings": {"outputSelection": {"bar.vy": ["abi"]}}} - sources = {"foo.vy": ""} + targets = [PurePath("foo.vy")] with pytest.raises(JSONError): - get_input_dict_output_formats(input_json, sources) + get_output_formats(input_json, targets) @pytest.mark.parametrize("output", TRANSLATE_MAP.items()) def test_translate_map(output): input_json = {"settings": {"outputSelection": {"foo.vy": [output[0]]}}} - sources = {"foo.vy": ""} - assert get_input_dict_output_formats(input_json, sources) == {"foo.vy": [output[1]]} + targets = [PurePath("foo.vy")] + assert get_output_formats(input_json, targets) == {PurePath("foo.vy"): [output[1]]} def test_star(): input_json = {"settings": {"outputSelection": {"*": ["*"]}}} - sources = {"foo.vy": "", "bar.vy": ""} + targets = [PurePath("foo.vy"), PurePath("bar.vy")] expected = sorted(set(TRANSLATE_MAP.values())) - result = get_input_dict_output_formats(input_json, sources) - assert result == {"foo.vy": expected, "bar.vy": expected} + result = get_output_formats(input_json, targets) + assert result == {PurePath("foo.vy"): expected, PurePath("bar.vy"): expected} def test_evm(): input_json = {"settings": {"outputSelection": {"foo.vy": ["abi", "evm"]}}} - sources = {"foo.vy": ""} + targets = [PurePath("foo.vy")] expected = ["abi"] + sorted(v for k, v in TRANSLATE_MAP.items() if k.startswith("evm")) - result = get_input_dict_output_formats(input_json, sources) - assert result == {"foo.vy": expected} + result = get_output_formats(input_json, targets) + assert result == {PurePath("foo.vy"): expected} def test_solc_style(): input_json = {"settings": {"outputSelection": {"foo.vy": {"": ["abi"], "foo.vy": ["ir"]}}}} - sources = {"foo.vy": ""} - assert get_input_dict_output_formats(input_json, sources) == {"foo.vy": ["abi", "ir_dict"]} + targets = [PurePath("foo.vy")] + assert get_output_formats(input_json, targets) == {PurePath("foo.vy"): ["abi", "ir_dict"]} def test_metadata(): input_json = {"settings": {"outputSelection": {"*": ["metadata"]}}} - sources = {"foo.vy": ""} - assert get_input_dict_output_formats(input_json, sources) == {"foo.vy": ["metadata"]} + targets = [PurePath("foo.vy")] + assert get_output_formats(input_json, targets) == {PurePath("foo.vy"): ["metadata"]} diff --git a/tests/cli/vyper_json/test_parse_args_vyperjson.py b/tests/cli/vyper_json/test_parse_args_vyperjson.py index 11e527843a..3b0f700c7e 100644 --- a/tests/cli/vyper_json/test_parse_args_vyperjson.py +++ b/tests/cli/vyper_json/test_parse_args_vyperjson.py @@ -29,7 +29,6 @@ def bar(a: uint256) -> bool: "inputs": [{"type": "uint256", "name": "a"}], "stateMutability": "nonpayable", "type": "function", - "gas": 313, } ] @@ -39,7 +38,7 @@ def bar(a: uint256) -> bool: "contracts/foo.vy": {"content": FOO_CODE}, "contracts/bar.vy": {"content": BAR_CODE}, }, - "interfaces": {"contracts/bar.json": {"abi": BAR_ABI}}, + "interfaces": {"contracts/ibar.json": {"abi": BAR_ABI}}, "settings": {"outputSelection": {"*": ["*"]}}, } diff --git a/tests/compiler/test_bytecode_runtime.py b/tests/compiler/test_bytecode_runtime.py index 9519b03772..613ee4d2b8 100644 --- a/tests/compiler/test_bytecode_runtime.py +++ b/tests/compiler/test_bytecode_runtime.py @@ -48,14 +48,14 @@ def _parse_cbor_metadata(initcode): def test_bytecode_runtime(): - out = vyper.compile_code(simple_contract_code, ["bytecode_runtime", "bytecode"]) + out = vyper.compile_code(simple_contract_code, output_formats=["bytecode_runtime", "bytecode"]) assert len(out["bytecode"]) > len(out["bytecode_runtime"]) assert out["bytecode_runtime"].removeprefix("0x") in out["bytecode"].removeprefix("0x") def test_bytecode_signature(): - out = vyper.compile_code(simple_contract_code, ["bytecode_runtime", "bytecode"]) + out = vyper.compile_code(simple_contract_code, output_formats=["bytecode_runtime", "bytecode"]) runtime_code = bytes.fromhex(out["bytecode_runtime"].removeprefix("0x")) initcode = bytes.fromhex(out["bytecode"].removeprefix("0x")) @@ -72,7 +72,9 @@ def test_bytecode_signature(): def test_bytecode_signature_dense_jumptable(): settings = Settings(optimize=OptimizationLevel.CODESIZE) - out = vyper.compile_code(many_functions, ["bytecode_runtime", "bytecode"], settings=settings) + out = vyper.compile_code( + many_functions, output_formats=["bytecode_runtime", "bytecode"], settings=settings + ) runtime_code = bytes.fromhex(out["bytecode_runtime"].removeprefix("0x")) initcode = bytes.fromhex(out["bytecode"].removeprefix("0x")) @@ -89,7 +91,9 @@ def test_bytecode_signature_dense_jumptable(): def test_bytecode_signature_sparse_jumptable(): settings = Settings(optimize=OptimizationLevel.GAS) - out = vyper.compile_code(many_functions, ["bytecode_runtime", "bytecode"], settings=settings) + out = vyper.compile_code( + many_functions, output_formats=["bytecode_runtime", "bytecode"], settings=settings + ) runtime_code = bytes.fromhex(out["bytecode_runtime"].removeprefix("0x")) initcode = bytes.fromhex(out["bytecode"].removeprefix("0x")) @@ -104,7 +108,7 @@ def test_bytecode_signature_sparse_jumptable(): def test_bytecode_signature_immutables(): - out = vyper.compile_code(has_immutables, ["bytecode_runtime", "bytecode"]) + out = vyper.compile_code(has_immutables, output_formats=["bytecode_runtime", "bytecode"]) runtime_code = bytes.fromhex(out["bytecode_runtime"].removeprefix("0x")) initcode = bytes.fromhex(out["bytecode"].removeprefix("0x")) diff --git a/tests/compiler/test_compile_code.py b/tests/compiler/test_compile_code.py index cdbf9d1f52..7af133e362 100644 --- a/tests/compiler/test_compile_code.py +++ b/tests/compiler/test_compile_code.py @@ -11,4 +11,4 @@ def a() -> bool: return True """ with pytest.warns(vyper.warnings.ContractSizeLimitWarning): - vyper.compile_code(code, ["bytecode_runtime"]) + vyper.compile_code(code, output_formats=["bytecode_runtime"]) diff --git a/tests/compiler/test_input_bundle.py b/tests/compiler/test_input_bundle.py new file mode 100644 index 0000000000..c49c81219b --- /dev/null +++ b/tests/compiler/test_input_bundle.py @@ -0,0 +1,208 @@ +import json +from pathlib import Path, PurePath + +import pytest + +from vyper.compiler.input_bundle import ABIInput, FileInput, FilesystemInputBundle, JSONInputBundle + + +# FilesystemInputBundle which uses same search path as make_file +@pytest.fixture +def input_bundle(tmp_path): + return FilesystemInputBundle([tmp_path]) + + +def test_load_file(make_file, input_bundle, tmp_path): + make_file("foo.vy", "contents") + + file = input_bundle.load_file(Path("foo.vy")) + + assert isinstance(file, FileInput) + assert file == FileInput(0, tmp_path / Path("foo.vy"), "contents") + + +def test_search_path_context_manager(make_file, tmp_path): + ib = FilesystemInputBundle([]) + + make_file("foo.vy", "contents") + + with pytest.raises(FileNotFoundError): + # no search path given + ib.load_file(Path("foo.vy")) + + with ib.search_path(tmp_path): + file = ib.load_file(Path("foo.vy")) + + assert isinstance(file, FileInput) + assert file == FileInput(0, tmp_path / Path("foo.vy"), "contents") + + +def test_search_path_precedence(make_file, tmp_path, tmp_path_factory, input_bundle): + # test search path precedence. + # most recent search path is the highest precedence + tmpdir = tmp_path_factory.mktemp("some_directory") + tmpdir2 = tmp_path_factory.mktemp("some_other_directory") + + for i, directory in enumerate([tmp_path, tmpdir, tmpdir2]): + with (directory / "foo.vy").open("w") as f: + f.write(f"contents {i}") + + ib = FilesystemInputBundle([tmp_path, tmpdir, tmpdir2]) + + file = ib.load_file("foo.vy") + + assert isinstance(file, FileInput) + assert file == FileInput(0, tmpdir2 / "foo.vy", "contents 2") + + with ib.search_path(tmpdir): + file = ib.load_file("foo.vy") + + assert isinstance(file, FileInput) + assert file == FileInput(1, tmpdir / "foo.vy", "contents 1") + + +# special rules for handling json files +def test_load_abi(make_file, input_bundle, tmp_path): + contents = json.dumps("some string") + + make_file("foo.json", contents) + + file = input_bundle.load_file("foo.json") + assert isinstance(file, ABIInput) + assert file == ABIInput(0, tmp_path / "foo.json", "some string") + + # suffix doesn't matter + make_file("foo.txt", contents) + + file = input_bundle.load_file("foo.txt") + assert isinstance(file, ABIInput) + assert file == ABIInput(1, tmp_path / "foo.txt", "some string") + + +# check that unique paths give unique source ids +def test_source_id_file_input(make_file, input_bundle, tmp_path): + make_file("foo.vy", "contents") + make_file("bar.vy", "contents 2") + + file = input_bundle.load_file("foo.vy") + assert file.source_id == 0 + assert file == FileInput(0, tmp_path / "foo.vy", "contents") + + file2 = input_bundle.load_file("bar.vy") + # source id increments + assert file2.source_id == 1 + assert file2 == FileInput(1, tmp_path / "bar.vy", "contents 2") + + file3 = input_bundle.load_file("foo.vy") + assert file3.source_id == 0 + assert file3 == FileInput(0, tmp_path / "foo.vy", "contents") + + +# check that unique paths give unique source ids +def test_source_id_json_input(make_file, input_bundle, tmp_path): + contents = json.dumps("some string") + contents2 = json.dumps(["some list"]) + + make_file("foo.json", contents) + + make_file("bar.json", contents2) + + file = input_bundle.load_file("foo.json") + assert isinstance(file, ABIInput) + assert file == ABIInput(0, tmp_path / "foo.json", "some string") + + file2 = input_bundle.load_file("bar.json") + assert isinstance(file2, ABIInput) + assert file2 == ABIInput(1, tmp_path / "bar.json", ["some list"]) + + file3 = input_bundle.load_file("foo.json") + assert isinstance(file3, ABIInput) + assert file3 == ABIInput(0, tmp_path / "foo.json", "some string") + + +# test some pathological case where the file changes underneath +def test_mutating_file_source_id(make_file, input_bundle, tmp_path): + make_file("foo.vy", "contents") + + file = input_bundle.load_file("foo.vy") + assert file.source_id == 0 + assert file == FileInput(0, tmp_path / "foo.vy", "contents") + + make_file("foo.vy", "new contents") + + file = input_bundle.load_file("foo.vy") + # source id hasn't changed, even though contents have + assert file.source_id == 0 + assert file == FileInput(0, tmp_path / "foo.vy", "new contents") + + +# test the os.normpath behavior of symlink +# (slightly pathological, for illustration's sake) +def test_load_file_symlink(make_file, input_bundle, tmp_path, tmp_path_factory): + dir1 = tmp_path / "first" + dir2 = tmp_path / "second" + symlink = tmp_path / "symlink" + + dir1.mkdir() + dir2.mkdir() + symlink.symlink_to(dir2, target_is_directory=True) + + with (tmp_path / "foo.vy").open("w") as f: + f.write("contents of the upper directory") + + with (dir1 / "foo.vy").open("w") as f: + f.write("contents of the inner directory") + + # symlink rules would be: + # base/symlink/../foo.vy => + # base/first/second/../foo.vy => + # base/first/foo.vy + # normpath would be base/symlink/../foo.vy => + # base/foo.vy + file = input_bundle.load_file(symlink / ".." / "foo.vy") + + assert file == FileInput(0, tmp_path / "foo.vy", "contents of the upper directory") + + +def test_json_input_bundle_basic(): + files = {PurePath("foo.vy"): {"content": "some text"}} + input_bundle = JSONInputBundle(files, [PurePath(".")]) + + file = input_bundle.load_file(PurePath("foo.vy")) + assert file == FileInput(0, PurePath("foo.vy"), "some text") + + +def test_json_input_bundle_normpath(): + files = {PurePath("foo/../bar.vy"): {"content": "some text"}} + input_bundle = JSONInputBundle(files, [PurePath(".")]) + + expected = FileInput(0, PurePath("bar.vy"), "some text") + + file = input_bundle.load_file(PurePath("bar.vy")) + assert file == expected + + file = input_bundle.load_file(PurePath("baz/../bar.vy")) + assert file == expected + + file = input_bundle.load_file(PurePath("./bar.vy")) + assert file == expected + + with input_bundle.search_path(PurePath("foo")): + file = input_bundle.load_file(PurePath("../bar.vy")) + assert file == expected + + +def test_json_input_abi(): + some_abi = ["some abi"] + some_abi_str = json.dumps(some_abi) + files = { + PurePath("foo.json"): {"abi": some_abi}, + PurePath("bar.txt"): {"content": some_abi_str}, + } + input_bundle = JSONInputBundle(files, [PurePath(".")]) + + file = input_bundle.load_file(PurePath("foo.json")) + assert file == ABIInput(0, PurePath("foo.json"), some_abi) + + file = input_bundle.load_file(PurePath("bar.txt")) + assert file == ABIInput(1, PurePath("bar.txt"), some_abi) diff --git a/tests/compiler/test_opcodes.py b/tests/compiler/test_opcodes.py index 20f45ced6b..15d2a617ba 100644 --- a/tests/compiler/test_opcodes.py +++ b/tests/compiler/test_opcodes.py @@ -22,7 +22,7 @@ def a() -> bool: return True """ - out = vyper.compile_code(code, ["opcodes_runtime", "opcodes"]) + out = vyper.compile_code(code, output_formats=["opcodes_runtime", "opcodes"]) assert len(out["opcodes"]) > len(out["opcodes_runtime"]) assert out["opcodes_runtime"] in out["opcodes"] diff --git a/tests/compiler/test_source_map.py b/tests/compiler/test_source_map.py index 886596bb80..c9a152b09c 100644 --- a/tests/compiler/test_source_map.py +++ b/tests/compiler/test_source_map.py @@ -28,7 +28,7 @@ def foo(a: uint256) -> int128: def test_jump_map(): - source_map = compile_code(TEST_CODE, ["source_map"])["source_map"] + source_map = compile_code(TEST_CODE, output_formats=["source_map"])["source_map"] pos_map = source_map["pc_pos_map"] jump_map = source_map["pc_jump_map"] @@ -46,7 +46,7 @@ def test_jump_map(): def test_pos_map_offsets(): - source_map = compile_code(TEST_CODE, ["source_map"])["source_map"] + source_map = compile_code(TEST_CODE, output_formats=["source_map"])["source_map"] expanded = expand_source_map(source_map["pc_pos_map_compressed"]) pc_iter = iter(source_map["pc_pos_map"][i] for i in sorted(source_map["pc_pos_map"])) @@ -76,7 +76,7 @@ def test_error_map(): def update_foo(): self.foo += 1 """ - error_map = compile_code(code, ["source_map"])["source_map"]["error_map"] + error_map = compile_code(code, output_formats=["source_map"])["source_map"]["error_map"] assert "safeadd" in list(error_map.values()) assert "fallback function" in list(error_map.values()) diff --git a/tests/conftest.py b/tests/conftest.py index c9d3f794a0..9b10b7c51c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,6 +11,7 @@ from vyper import compiler from vyper.codegen.ir_node import IRnode +from vyper.compiler.input_bundle import FilesystemInputBundle from vyper.compiler.settings import OptimizationLevel, _set_debug_mode from vyper.ir import compile_ir, optimizer @@ -70,6 +71,34 @@ def keccak(): return Web3.keccak +@pytest.fixture +def make_file(tmp_path): + # writes file_contents to file_name, creating it in the + # tmp_path directory. returns final path. + def fn(file_name, file_contents): + path = tmp_path / file_name + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w") as f: + f.write(file_contents) + + return path + + return fn + + +# this can either be used for its side effects (to prepare a call +# to get_contract), or the result can be provided directly to +# compile_code / CompilerData. +@pytest.fixture +def make_input_bundle(tmp_path, make_file): + def fn(sources_dict): + for file_name, file_contents in sources_dict.items(): + make_file(file_name, file_contents) + return FilesystemInputBundle([tmp_path]) + + return fn + + @pytest.fixture def bytes_helper(): def bytes_helper(str, length): diff --git a/tests/parser/ast_utils/test_ast_dict.py b/tests/parser/ast_utils/test_ast_dict.py index f483d0cbe8..1f60c9ac8b 100644 --- a/tests/parser/ast_utils/test_ast_dict.py +++ b/tests/parser/ast_utils/test_ast_dict.py @@ -19,7 +19,7 @@ def get_node_ids(ast_struct, ids=None): elif v is None or isinstance(v, (str, int)): continue else: - raise Exception("Unknown ast_struct provided.") + raise Exception(f"Unknown ast_struct provided. {k}, {v}") return ids @@ -30,7 +30,7 @@ def test() -> int128: a: uint256 = 100 return 123 """ - dict_out = compiler.compile_code(code, ["ast_dict"]) + dict_out = compiler.compile_code(code, output_formats=["ast_dict"]) node_ids = get_node_ids(dict_out) assert len(node_ids) == len(set(node_ids)) @@ -40,7 +40,7 @@ def test_basic_ast(): code = """ a: int128 """ - dict_out = compiler.compile_code(code, ["ast_dict"]) + dict_out = compiler.compile_code(code, output_formats=["ast_dict"]) assert dict_out["ast_dict"]["ast"]["body"][0] == { "annotation": { "ast_type": "Name", @@ -89,7 +89,7 @@ def foo() -> uint256: view def foo() -> uint256: return 1 """ - dict_out = compiler.compile_code(code, ["ast_dict"]) + dict_out = compiler.compile_code(code, output_formats=["ast_dict"]) assert dict_out["ast_dict"]["ast"]["body"][1] == { "col_offset": 0, "annotation": { diff --git a/tests/parser/features/test_init.py b/tests/parser/features/test_init.py index 83bcbc95ea..29a466e869 100644 --- a/tests/parser/features/test_init.py +++ b/tests/parser/features/test_init.py @@ -15,7 +15,7 @@ def __init__(a: uint256): assert c.val() == 123 # Make sure the init code does not access calldata - assembly = vyper.compile_code(code, ["asm"])["asm"].split(" ") + assembly = vyper.compile_code(code, output_formats=["asm"])["asm"].split(" ") ir_return_idx_start = assembly.index("{") ir_return_idx_end = assembly.index("}") diff --git a/tests/parser/functions/test_bitwise.py b/tests/parser/functions/test_bitwise.py index 3ba74034ac..1d62a5be79 100644 --- a/tests/parser/functions/test_bitwise.py +++ b/tests/parser/functions/test_bitwise.py @@ -32,7 +32,7 @@ def _shr(x: uint256, y: uint256) -> uint256: def test_bitwise_opcodes(): - opcodes = compile_code(code, ["opcodes"])["opcodes"] + opcodes = compile_code(code, output_formats=["opcodes"])["opcodes"] assert "SHL" in opcodes assert "SHR" in opcodes diff --git a/tests/parser/functions/test_interfaces.py b/tests/parser/functions/test_interfaces.py index c16e188cfd..8cb0124f29 100644 --- a/tests/parser/functions/test_interfaces.py +++ b/tests/parser/functions/test_interfaces.py @@ -1,10 +1,15 @@ +import json from decimal import Decimal import pytest -from vyper.cli.utils import extract_file_interface_imports -from vyper.compiler import compile_code, compile_codes -from vyper.exceptions import ArgumentException, InterfaceViolation, StructureException +from vyper.compiler import compile_code +from vyper.exceptions import ( + ArgumentException, + InterfaceViolation, + NamespaceCollision, + StructureException, +) def test_basic_extract_interface(): @@ -24,7 +29,7 @@ def allowance(_owner: address, _spender: address) -> (uint256, uint256): return 1, 2 """ - out = compile_code(code, ["interface"]) + out = compile_code(code, output_formats=["interface"]) out = out["interface"] code_pass = "\n".join(code.split("\n")[:-2] + [" pass"]) # replace with a pass statement. @@ -55,8 +60,9 @@ def allowance(_owner: address, _spender: address) -> (uint256, uint256): view def test(_owner: address): nonpayable """ - out = compile_codes({"one.vy": code}, ["external_interface"])["one.vy"] - out = out["external_interface"] + out = compile_code(code, contract_name="One.vy", output_formats=["external_interface"])[ + "external_interface" + ] assert interface.strip() == out.strip() @@ -75,7 +81,7 @@ def test() -> bool: assert_compile_failed(lambda: compile_code(code), InterfaceViolation) -def test_external_interface_parsing(assert_compile_failed): +def test_external_interface_parsing(make_input_bundle, assert_compile_failed): interface_code = """ @external def foo() -> uint256: @@ -86,7 +92,7 @@ def bar() -> uint256: pass """ - interface_codes = {"FooBarInterface": {"type": "vyper", "code": interface_code}} + input_bundle = make_input_bundle({"a.vy": interface_code}) code = """ import a as FooBarInterface @@ -102,7 +108,7 @@ def bar() -> uint256: return 2 """ - assert compile_code(code, interface_codes=interface_codes) + assert compile_code(code, input_bundle=input_bundle) not_implemented_code = """ import a as FooBarInterface @@ -116,18 +122,17 @@ def foo() -> uint256: """ assert_compile_failed( - lambda: compile_code(not_implemented_code, interface_codes=interface_codes), - InterfaceViolation, + lambda: compile_code(not_implemented_code, input_bundle=input_bundle), InterfaceViolation ) -def test_missing_event(assert_compile_failed): +def test_missing_event(make_input_bundle, assert_compile_failed): interface_code = """ event Foo: a: uint256 """ - interface_codes = {"FooBarInterface": {"type": "vyper", "code": interface_code}} + input_bundle = make_input_bundle({"a.vy": interface_code}) not_implemented_code = """ import a as FooBarInterface @@ -140,19 +145,18 @@ def bar() -> uint256: """ assert_compile_failed( - lambda: compile_code(not_implemented_code, interface_codes=interface_codes), - InterfaceViolation, + lambda: compile_code(not_implemented_code, input_bundle=input_bundle), InterfaceViolation ) # check that event types match -def test_malformed_event(assert_compile_failed): +def test_malformed_event(make_input_bundle, assert_compile_failed): interface_code = """ event Foo: a: uint256 """ - interface_codes = {"FooBarInterface": {"type": "vyper", "code": interface_code}} + input_bundle = make_input_bundle({"a.vy": interface_code}) not_implemented_code = """ import a as FooBarInterface @@ -168,19 +172,18 @@ def bar() -> uint256: """ assert_compile_failed( - lambda: compile_code(not_implemented_code, interface_codes=interface_codes), - InterfaceViolation, + lambda: compile_code(not_implemented_code, input_bundle=input_bundle), InterfaceViolation ) # check that event non-indexed arg needs to match interface -def test_malformed_events_indexed(assert_compile_failed): +def test_malformed_events_indexed(make_input_bundle, assert_compile_failed): interface_code = """ event Foo: a: uint256 """ - interface_codes = {"FooBarInterface": {"type": "vyper", "code": interface_code}} + input_bundle = make_input_bundle({"a.vy": interface_code}) not_implemented_code = """ import a as FooBarInterface @@ -197,19 +200,18 @@ def bar() -> uint256: """ assert_compile_failed( - lambda: compile_code(not_implemented_code, interface_codes=interface_codes), - InterfaceViolation, + lambda: compile_code(not_implemented_code, input_bundle=input_bundle), InterfaceViolation ) # check that event indexed arg needs to match interface -def test_malformed_events_indexed2(assert_compile_failed): +def test_malformed_events_indexed2(make_input_bundle, assert_compile_failed): interface_code = """ event Foo: a: indexed(uint256) """ - interface_codes = {"FooBarInterface": {"type": "vyper", "code": interface_code}} + input_bundle = make_input_bundle({"a.vy": interface_code}) not_implemented_code = """ import a as FooBarInterface @@ -226,43 +228,47 @@ def bar() -> uint256: """ assert_compile_failed( - lambda: compile_code(not_implemented_code, interface_codes=interface_codes), - InterfaceViolation, + lambda: compile_code(not_implemented_code, input_bundle=input_bundle), InterfaceViolation ) VALID_IMPORT_CODE = [ # import statement, import path without suffix - ("import a as Foo", "a"), - ("import b.a as Foo", "b/a"), - ("import Foo as Foo", "Foo"), - ("from a import Foo", "a/Foo"), - ("from b.a import Foo", "b/a/Foo"), - ("from .a import Foo", "./a/Foo"), - ("from ..a import Foo", "../a/Foo"), + ("import a as Foo", "a.vy"), + ("import b.a as Foo", "b/a.vy"), + ("import Foo as Foo", "Foo.vy"), + ("from a import Foo", "a/Foo.vy"), + ("from b.a import Foo", "b/a/Foo.vy"), + ("from .a import Foo", "./a/Foo.vy"), + ("from ..a import Foo", "../a/Foo.vy"), ] -@pytest.mark.parametrize("code", VALID_IMPORT_CODE) -def test_extract_file_interface_imports(code): - assert extract_file_interface_imports(code[0]) == {"Foo": code[1]} +@pytest.mark.parametrize("code,filename", VALID_IMPORT_CODE) +def test_extract_file_interface_imports(code, filename, make_input_bundle): + input_bundle = make_input_bundle({filename: ""}) + + assert compile_code(code, input_bundle=input_bundle) is not None BAD_IMPORT_CODE = [ - "import a", # must alias absolute imports - "import a as A\nimport a as A", # namespace collisions - "from b import a\nfrom a import a", - "from . import a\nimport a as a", - "import a as a\nfrom . import a", + ("import a", StructureException), # must alias absolute imports + ("import a as A\nimport a as A", NamespaceCollision), + ("from b import a\nfrom . import a", NamespaceCollision), + ("from . import a\nimport a as a", NamespaceCollision), + ("import a as a\nfrom . import a", NamespaceCollision), ] -@pytest.mark.parametrize("code", BAD_IMPORT_CODE) -def test_extract_file_interface_imports_raises(code, assert_compile_failed): - assert_compile_failed(lambda: extract_file_interface_imports(code), StructureException) +@pytest.mark.parametrize("code,exception_type", BAD_IMPORT_CODE) +def test_extract_file_interface_imports_raises( + code, exception_type, assert_compile_failed, make_input_bundle +): + input_bundle = make_input_bundle({"a.vy": "", "b/a.vy": ""}) # dummy + assert_compile_failed(lambda: compile_code(code, input_bundle=input_bundle), exception_type) -def test_external_call_to_interface(w3, get_contract): +def test_external_call_to_interface(w3, get_contract, make_input_bundle): token_code = """ balanceOf: public(HashMap[address, uint256]) @@ -271,6 +277,8 @@ def transfer(to: address, _value: uint256): self.balanceOf[to] += _value """ + input_bundle = make_input_bundle({"one.vy": token_code}) + code = """ import one as TokenCode @@ -292,9 +300,7 @@ def test(): """ erc20 = get_contract(token_code) - test_c = get_contract( - code, *[erc20.address], interface_codes={"TokenCode": {"type": "vyper", "code": token_code}} - ) + test_c = get_contract(code, *[erc20.address], input_bundle=input_bundle) sender = w3.eth.accounts[0] assert erc20.balanceOf(sender) == 0 @@ -313,7 +319,7 @@ def test(): ("epsilon(decimal)", "decimal", Decimal("1E-10")), ], ) -def test_external_call_to_interface_kwarg(get_contract, kwarg, typ, expected): +def test_external_call_to_interface_kwarg(get_contract, kwarg, typ, expected, make_input_bundle): code_a = f""" @external @view @@ -321,6 +327,8 @@ def foo(_max: {typ} = {kwarg}) -> {typ}: return _max """ + input_bundle = make_input_bundle({"one.vy": code_a}) + code_b = f""" import one as ContractA @@ -331,11 +339,7 @@ def bar(a_address: address) -> {typ}: """ contract_a = get_contract(code_a) - contract_b = get_contract( - code_b, - *[contract_a.address], - interface_codes={"ContractA": {"type": "vyper", "code": code_a}}, - ) + contract_b = get_contract(code_b, *[contract_a.address], input_bundle=input_bundle) assert contract_b.bar(contract_a.address) == expected @@ -368,9 +372,7 @@ def test(): """ erc20 = get_contract(token_code) - test_c = get_contract( - code, *[erc20.address], interface_codes={"TokenCode": {"type": "vyper", "code": token_code}} - ) + test_c = get_contract(code, *[erc20.address]) sender = w3.eth.accounts[0] assert erc20.balanceOf(sender) == 0 @@ -440,11 +442,7 @@ def test_fail3() -> int256: """ bad_c = get_contract(external_contract) - c = get_contract( - code, - bad_c.address, - interface_codes={"BadCode": {"type": "vyper", "code": external_contract}}, - ) + c = get_contract(code, bad_c.address) assert bad_c.ok() == 1 assert bad_c.should_fail() == -(2**255) @@ -502,7 +500,9 @@ def test_fail2() -> Bytes[3]: # test data returned from external interface gets clamped -def test_json_abi_bytes_clampers(get_contract, assert_tx_failed, assert_compile_failed): +def test_json_abi_bytes_clampers( + get_contract, assert_tx_failed, assert_compile_failed, make_input_bundle +): external_contract = """ @external def returns_Bytes3() -> Bytes[3]: @@ -546,18 +546,15 @@ def test_fail3() -> Bytes[3]: """ bad_c = get_contract(external_contract) - bad_c_interface = { - "BadJSONInterface": { - "type": "json", - "code": compile_code(external_contract, ["abi"])["abi"], - } - } + + bad_json_interface = json.dumps(compile_code(external_contract, output_formats=["abi"])["abi"]) + input_bundle = make_input_bundle({"BadJSONInterface.json": bad_json_interface}) assert_compile_failed( - lambda: get_contract(should_not_compile, interface_codes=bad_c_interface), ArgumentException + lambda: get_contract(should_not_compile, input_bundle=input_bundle), ArgumentException ) - c = get_contract(code, bad_c.address, interface_codes=bad_c_interface) + c = get_contract(code, bad_c.address, input_bundle=input_bundle) assert bad_c.returns_Bytes3() == b"123" assert_tx_failed(lambda: c.test_fail1()) @@ -565,7 +562,7 @@ def test_fail3() -> Bytes[3]: assert_tx_failed(lambda: c.test_fail3()) -def test_units_interface(w3, get_contract): +def test_units_interface(w3, get_contract, make_input_bundle): code = """ import balanceof as BalanceOf @@ -576,49 +573,41 @@ def test_units_interface(w3, get_contract): def balanceOf(owner: address) -> uint256: return as_wei_value(1, "ether") """ + interface_code = """ @external @view def balanceOf(owner: address) -> uint256: pass """ - interface_codes = {"BalanceOf": {"type": "vyper", "code": interface_code}} - c = get_contract(code, interface_codes=interface_codes) + + input_bundle = make_input_bundle({"balanceof.vy": interface_code}) + + c = get_contract(code, input_bundle=input_bundle) assert c.balanceOf(w3.eth.accounts[0]) == w3.to_wei(1, "ether") -def test_local_and_global_interface_namespaces(): +def test_simple_implements(make_input_bundle): interface_code = """ @external def foo() -> uint256: pass """ - global_interface_codes = { - "FooInterface": {"type": "vyper", "code": interface_code}, - "BarInterface": {"type": "vyper", "code": interface_code}, - } - local_interface_codes = { - "FooContract": {"FooInterface": {"type": "vyper", "code": interface_code}}, - "BarContract": {"BarInterface": {"type": "vyper", "code": interface_code}}, - } - code = """ -import a as {0} +import a as FooInterface -implements: {0} +implements: FooInterface @external def foo() -> uint256: return 1 """ - codes = {"FooContract": code.format("FooInterface"), "BarContract": code.format("BarInterface")} + input_bundle = make_input_bundle({"a.vy": interface_code}) - global_compiled = compile_codes(codes, interface_codes=global_interface_codes) - local_compiled = compile_codes(codes, interface_codes=local_interface_codes) - assert global_compiled == local_compiled + assert compile_code(code, input_bundle=input_bundle) is not None def test_self_interface_is_allowed(get_contract): @@ -724,20 +713,28 @@ def convert_v1_abi(abi): @pytest.mark.parametrize("type_str", [i[0] for i in type_str_params]) -def test_json_interface_implements(type_str): +def test_json_interface_implements(type_str, make_input_bundle, make_file): code = interface_test_code.format(type_str) - abi = compile_code(code, ["abi"])["abi"] + abi = compile_code(code, output_formats=["abi"])["abi"] + code = f"import jsonabi as jsonabi\nimplements: jsonabi\n{code}" - compile_code(code, interface_codes={"jsonabi": {"type": "json", "code": abi}}) - compile_code(code, interface_codes={"jsonabi": {"type": "json", "code": convert_v1_abi(abi)}}) + + input_bundle = make_input_bundle({"jsonabi.json": json.dumps(abi)}) + + compile_code(code, input_bundle=input_bundle) + + # !!! overwrite the file + make_file("jsonabi.json", json.dumps(convert_v1_abi(abi))) + + compile_code(code, input_bundle=input_bundle) @pytest.mark.parametrize("type_str,value", type_str_params) -def test_json_interface_calls(get_contract, type_str, value): +def test_json_interface_calls(get_contract, type_str, value, make_input_bundle, make_file): code = interface_test_code.format(type_str) - abi = compile_code(code, ["abi"])["abi"] + abi = compile_code(code, output_formats=["abi"])["abi"] c1 = get_contract(code) code = f""" @@ -748,9 +745,11 @@ def test_json_interface_calls(get_contract, type_str, value): def test_call(a: address, b: {type_str}) -> {type_str}: return jsonabi(a).test_json(b) """ - c2 = get_contract(code, interface_codes={"jsonabi": {"type": "json", "code": abi}}) + input_bundle = make_input_bundle({"jsonabi.json": json.dumps(abi)}) + + c2 = get_contract(code, input_bundle=input_bundle) assert c2.test_call(c1.address, value) == value - c3 = get_contract( - code, interface_codes={"jsonabi": {"type": "json", "code": convert_v1_abi(abi)}} - ) + + make_file("jsonabi.json", json.dumps(convert_v1_abi(abi))) + c3 = get_contract(code, input_bundle=input_bundle) assert c3.test_call(c1.address, value) == value diff --git a/tests/parser/functions/test_raw_call.py b/tests/parser/functions/test_raw_call.py index 81efe64a18..5bb23447e4 100644 --- a/tests/parser/functions/test_raw_call.py +++ b/tests/parser/functions/test_raw_call.py @@ -274,8 +274,8 @@ def test_raw_call(_target: address): def test_raw_call(_target: address): raw_call(_target, method_id("foo()"), max_outsize=0) """ - output1 = compile_code(code1, ["bytecode", "bytecode_runtime"]) - output2 = compile_code(code2, ["bytecode", "bytecode_runtime"]) + output1 = compile_code(code1, output_formats=["bytecode", "bytecode_runtime"]) + output2 = compile_code(code2, output_formats=["bytecode", "bytecode_runtime"]) assert output1 == output2 @@ -296,8 +296,8 @@ def test_raw_call(_target: address) -> bool: a: bool = raw_call(_target, method_id("foo()"), max_outsize=0, revert_on_failure=False) return a """ - output1 = compile_code(code1, ["bytecode", "bytecode_runtime"]) - output2 = compile_code(code2, ["bytecode", "bytecode_runtime"]) + output1 = compile_code(code1, output_formats=["bytecode", "bytecode_runtime"]) + output2 = compile_code(code2, output_formats=["bytecode", "bytecode_runtime"]) assert output1 == output2 diff --git a/tests/parser/functions/test_return_struct.py b/tests/parser/functions/test_return_struct.py index 425caedb75..cdd8342d8a 100644 --- a/tests/parser/functions/test_return_struct.py +++ b/tests/parser/functions/test_return_struct.py @@ -17,7 +17,7 @@ def test() -> Voter: return a """ - out = compile_code(code, ["abi"]) + out = compile_code(code, output_formats=["abi"]) abi = out["abi"][0] assert abi["name"] == "test" @@ -38,7 +38,7 @@ def test() -> Voter: return a """ - out = compile_code(code, ["abi"]) + out = compile_code(code, output_formats=["abi"]) abi = out["abi"][0] assert abi["name"] == "test" diff --git a/tests/parser/syntax/test_codehash.py b/tests/parser/syntax/test_codehash.py index 5074d14636..c2d9a2e274 100644 --- a/tests/parser/syntax/test_codehash.py +++ b/tests/parser/syntax/test_codehash.py @@ -33,7 +33,7 @@ def foo4() -> bytes32: return self.a.codehash """ settings = Settings(evm_version=evm_version, optimize=optimize) - compiled = compile_code(code, ["bytecode_runtime"], settings=settings) + compiled = compile_code(code, output_formats=["bytecode_runtime"], settings=settings) bytecode = bytes.fromhex(compiled["bytecode_runtime"][2:]) hash_ = keccak256(bytecode) diff --git a/tests/parser/syntax/test_interfaces.py b/tests/parser/syntax/test_interfaces.py index 498f1363d8..9100389dbd 100644 --- a/tests/parser/syntax/test_interfaces.py +++ b/tests/parser/syntax/test_interfaces.py @@ -374,7 +374,7 @@ def test_interfaces_success(good_code): assert compiler.compile_code(good_code) is not None -def test_imports_and_implements_within_interface(): +def test_imports_and_implements_within_interface(make_input_bundle): interface_code = """ from vyper.interfaces import ERC20 import foo.bar as Baz @@ -386,6 +386,8 @@ def foobar(): pass """ + input_bundle = make_input_bundle({"foo.vy": interface_code}) + code = """ import foo as Foo @@ -396,9 +398,4 @@ def foobar(): pass """ - assert ( - compiler.compile_code( - code, interface_codes={"Foo": {"type": "vyper", "code": interface_code}} - ) - is not None - ) + assert compiler.compile_code(code, input_bundle=input_bundle) is not None diff --git a/tests/parser/syntax/test_self_balance.py b/tests/parser/syntax/test_self_balance.py index 63db58e347..d22d8a2750 100644 --- a/tests/parser/syntax/test_self_balance.py +++ b/tests/parser/syntax/test_self_balance.py @@ -20,7 +20,7 @@ def __default__(): pass """ settings = Settings(evm_version=evm_version) - opcodes = compiler.compile_code(code, ["opcodes"], settings=settings)["opcodes"] + opcodes = compiler.compile_code(code, output_formats=["opcodes"], settings=settings)["opcodes"] if EVM_VERSIONS[evm_version] >= EVM_VERSIONS["istanbul"]: assert "SELFBALANCE" in opcodes else: diff --git a/tests/parser/test_selector_table_stability.py b/tests/parser/test_selector_table_stability.py index abc2c17b8f..3302ff5009 100644 --- a/tests/parser/test_selector_table_stability.py +++ b/tests/parser/test_selector_table_stability.py @@ -8,7 +8,9 @@ def test_dense_jumptable_stability(): code = "\n".join(f"@external\ndef {name}():\n pass" for name in function_names) - output = compile_code(code, ["asm"], settings=Settings(optimize=OptimizationLevel.CODESIZE)) + output = compile_code( + code, output_formats=["asm"], settings=Settings(optimize=OptimizationLevel.CODESIZE) + ) # test that the selector table data is stable across different runs # (tox should provide different PYTHONHASHSEEDs). diff --git a/tests/parser/types/numbers/test_constants.py b/tests/parser/types/numbers/test_constants.py index 652c8e8bd9..25617651ec 100644 --- a/tests/parser/types/numbers/test_constants.py +++ b/tests/parser/types/numbers/test_constants.py @@ -206,7 +206,7 @@ def test() -> uint256: return ret """ - ir = compile_code(code, ["ir"])["ir"] + ir = compile_code(code, output_formats=["ir"])["ir"] assert search_for_sublist( ir, ["mstore", [MemoryPositions.RESERVED_MEMORY], [2**12 * some_prime]] ) diff --git a/vyper/__init__.py b/vyper/__init__.py index 35237bd044..482d5c3a60 100644 --- a/vyper/__init__.py +++ b/vyper/__init__.py @@ -1,6 +1,6 @@ from pathlib import Path as _Path -from vyper.compiler import compile_code, compile_codes # noqa: F401 +from vyper.compiler import compile_code # noqa: F401 try: from importlib.metadata import PackageNotFoundError # type: ignore diff --git a/vyper/builtins/interfaces/ERC165.py b/vyper/builtins/interfaces/ERC165.vy similarity index 75% rename from vyper/builtins/interfaces/ERC165.py rename to vyper/builtins/interfaces/ERC165.vy index 0a75431f3c..a4ca451abd 100644 --- a/vyper/builtins/interfaces/ERC165.py +++ b/vyper/builtins/interfaces/ERC165.vy @@ -1,6 +1,4 @@ -interface_code = """ @view @external def supportsInterface(interface_id: bytes4) -> bool: pass -""" diff --git a/vyper/builtins/interfaces/ERC20.py b/vyper/builtins/interfaces/ERC20.vy similarity index 96% rename from vyper/builtins/interfaces/ERC20.py rename to vyper/builtins/interfaces/ERC20.vy index a63408672b..065ca97a9b 100644 --- a/vyper/builtins/interfaces/ERC20.py +++ b/vyper/builtins/interfaces/ERC20.vy @@ -1,4 +1,3 @@ -interface_code = """ # Events event Transfer: _from: indexed(address) @@ -37,4 +36,3 @@ def transferFrom(_from: address, _to: address, _value: uint256) -> bool: @external def approve(_spender: address, _value: uint256) -> bool: pass -""" diff --git a/vyper/builtins/interfaces/ERC20Detailed.py b/vyper/builtins/interfaces/ERC20Detailed.py deleted file mode 100644 index 03dd597e8a..0000000000 --- a/vyper/builtins/interfaces/ERC20Detailed.py +++ /dev/null @@ -1,22 +0,0 @@ -""" -NOTE: interface uses `String[1]` where 1 is the lower bound of the string returned by the function. - For end-users this means they can't use `implements: ERC20Detailed` unless their implementation - uses a value n >= 1. Regardless this is fine as one can't do String[0] where n == 0. -""" - -interface_code = """ -@view -@external -def name() -> String[1]: - pass - -@view -@external -def symbol() -> String[1]: - pass - -@view -@external -def decimals() -> uint8: - pass -""" diff --git a/vyper/builtins/interfaces/ERC20Detailed.vy b/vyper/builtins/interfaces/ERC20Detailed.vy new file mode 100644 index 0000000000..7c4f546d45 --- /dev/null +++ b/vyper/builtins/interfaces/ERC20Detailed.vy @@ -0,0 +1,18 @@ +#NOTE: interface uses `String[1]` where 1 is the lower bound of the string returned by the function. +# For end-users this means they can't use `implements: ERC20Detailed` unless their implementation +# uses a value n >= 1. Regardless this is fine as one can't do String[0] where n == 0. + +@view +@external +def name() -> String[1]: + pass + +@view +@external +def symbol() -> String[1]: + pass + +@view +@external +def decimals() -> uint8: + pass diff --git a/vyper/builtins/interfaces/ERC4626.py b/vyper/builtins/interfaces/ERC4626.vy similarity index 98% rename from vyper/builtins/interfaces/ERC4626.py rename to vyper/builtins/interfaces/ERC4626.vy index 21a9ce723a..05865406cf 100644 --- a/vyper/builtins/interfaces/ERC4626.py +++ b/vyper/builtins/interfaces/ERC4626.vy @@ -1,4 +1,3 @@ -interface_code = """ # Events event Deposit: sender: indexed(address) @@ -89,4 +88,3 @@ def previewRedeem(shares: uint256) -> uint256: @external def redeem(shares: uint256, receiver: address=msg.sender, owner: address=msg.sender) -> uint256: pass -""" diff --git a/vyper/builtins/interfaces/ERC721.py b/vyper/builtins/interfaces/ERC721.vy similarity index 97% rename from vyper/builtins/interfaces/ERC721.py rename to vyper/builtins/interfaces/ERC721.vy index 8dea4e4976..464c0e255b 100644 --- a/vyper/builtins/interfaces/ERC721.py +++ b/vyper/builtins/interfaces/ERC721.vy @@ -1,4 +1,3 @@ -interface_code = """ # Events event Transfer: @@ -66,5 +65,3 @@ def approve(_approved: address, _tokenId: uint256): @external def setApprovalForAll(_operator: address, _approved: bool): pass - -""" diff --git a/vyper/builtins/interfaces/__init__.py b/vyper/builtins/interfaces/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/vyper/cli/utils.py b/vyper/cli/utils.py deleted file mode 100644 index 1110ecdfdd..0000000000 --- a/vyper/cli/utils.py +++ /dev/null @@ -1,58 +0,0 @@ -from pathlib import Path -from typing import Sequence - -from vyper import ast as vy_ast -from vyper.exceptions import StructureException -from vyper.typing import InterfaceImports, SourceCode - - -def get_interface_file_path(base_paths: Sequence, import_path: str) -> Path: - relative_path = Path(import_path) - for path in base_paths: - # Find ABI JSON files - file_path = path.joinpath(relative_path) - suffix = next((i for i in (".vy", ".json") if file_path.with_suffix(i).exists()), None) - if suffix: - return file_path.with_suffix(suffix) - - # Find ethPM Manifest files (`from path.to.Manifest import InterfaceName`) - # NOTE: Use file parent because this assumes that `file_path` - # coincides with an ABI interface file - file_path = file_path.parent - suffix = next((i for i in (".vy", ".json") if file_path.with_suffix(i).exists()), None) - if suffix: - return file_path.with_suffix(suffix) - - raise FileNotFoundError(f" Cannot locate interface '{import_path}{{.vy,.json}}'") - - -def extract_file_interface_imports(code: SourceCode) -> InterfaceImports: - ast_tree = vy_ast.parse_to_ast(code) - - imports_dict: InterfaceImports = {} - for node in ast_tree.get_children((vy_ast.Import, vy_ast.ImportFrom)): - if isinstance(node, vy_ast.Import): # type: ignore - if not node.alias: - raise StructureException("Import requires an accompanying `as` statement", node) - if node.alias in imports_dict: - raise StructureException(f"Interface with alias {node.alias} already exists", node) - imports_dict[node.alias] = node.name.replace(".", "/") - elif isinstance(node, vy_ast.ImportFrom): # type: ignore - level = node.level # type: ignore - module = node.module or "" # type: ignore - if not level and module == "vyper.interfaces": - # uses a builtin interface, so skip adding to imports - continue - - base_path = "" - if level > 1: - base_path = "../" * (level - 1) - elif level == 1: - base_path = "./" - base_path = f"{base_path}{module.replace('.','/')}/" - - if node.name in imports_dict and imports_dict[node.name] != f"{base_path}{node.name}": - raise StructureException(f"Interface with name {node.name} already exists", node) - imports_dict[node.name] = f"{base_path}{node.name}" - - return imports_dict diff --git a/vyper/cli/vyper_compile.py b/vyper/cli/vyper_compile.py index bdd01eebbe..c4f60660cb 100755 --- a/vyper/cli/vyper_compile.py +++ b/vyper/cli/vyper_compile.py @@ -3,14 +3,13 @@ import json import sys import warnings -from collections import OrderedDict from pathlib import Path -from typing import Dict, Iterable, Iterator, Optional, Set, TypeVar +from typing import Any, Iterable, Iterator, Optional, Set, TypeVar import vyper import vyper.codegen.ir_node as ir_node from vyper.cli import vyper_json -from vyper.cli.utils import extract_file_interface_imports, get_interface_file_path +from vyper.compiler.input_bundle import FileInput, FilesystemInputBundle from vyper.compiler.settings import ( VYPER_TRACEBACK_LIMIT, OptimizationLevel, @@ -18,7 +17,7 @@ _set_debug_mode, ) from vyper.evm.opcodes import DEFAULT_EVM_VERSION, EVM_VERSIONS -from vyper.typing import ContractCodes, ContractPath, OutputFormats +from vyper.typing import ContractPath, OutputFormats T = TypeVar("T") @@ -219,94 +218,20 @@ def exc_handler(contract_path: ContractPath, exception: Exception) -> None: raise exception -def get_interface_codes(root_path: Path, contract_sources: ContractCodes) -> Dict: - interface_codes: Dict = {} - interfaces: Dict = {} - - for file_path, code in contract_sources.items(): - interfaces[file_path] = {} - parent_path = root_path.joinpath(file_path).parent - - interface_codes = extract_file_interface_imports(code) - for interface_name, interface_path in interface_codes.items(): - base_paths = [parent_path] - if not interface_path.startswith(".") and root_path.joinpath(file_path).exists(): - base_paths.append(root_path) - elif interface_path.startswith("../") and len(Path(file_path).parent.parts) < Path( - interface_path - ).parts.count(".."): - raise FileNotFoundError( - f"{file_path} - Cannot perform relative import outside of base folder" - ) - - valid_path = get_interface_file_path(base_paths, interface_path) - with valid_path.open() as fh: - code = fh.read() - if valid_path.suffix == ".json": - contents = json.loads(code.encode()) - - # EthPM Manifest (EIP-2678) - if "contractTypes" in contents: - if ( - interface_name not in contents["contractTypes"] - or "abi" not in contents["contractTypes"][interface_name] - ): - raise ValueError( - f"Could not find interface '{interface_name}'" - f" in manifest '{valid_path}'." - ) - - interfaces[file_path][interface_name] = { - "type": "json", - "code": contents["contractTypes"][interface_name]["abi"], - } - - # ABI JSON file (either `List[ABI]` or `{"abi": List[ABI]}`) - elif isinstance(contents, list) or ( - "abi" in contents and isinstance(contents["abi"], list) - ): - interfaces[file_path][interface_name] = {"type": "json", "code": contents} - - else: - raise ValueError(f"Corrupted file: '{valid_path}'") - - else: - interfaces[file_path][interface_name] = {"type": "vyper", "code": code} - - return interfaces - - def compile_files( - input_files: Iterable[str], + input_files: list[str], output_formats: OutputFormats, root_folder: str = ".", show_gas_estimates: bool = False, settings: Optional[Settings] = None, - storage_layout: Optional[Iterable[str]] = None, + storage_layout_paths: list[str] = None, no_bytecode_metadata: bool = False, -) -> OrderedDict: +) -> dict: root_path = Path(root_folder).resolve() if not root_path.exists(): raise FileNotFoundError(f"Invalid root path - '{root_path.as_posix()}' does not exist") - contract_sources: ContractCodes = OrderedDict() - for file_name in input_files: - file_path = Path(file_name) - try: - file_str = file_path.resolve().relative_to(root_path).as_posix() - except ValueError: - file_str = file_path.as_posix() - with file_path.open() as fh: - # trailing newline fixes python parsing bug when source ends in a comment - # https://bugs.python.org/issue35107 - contract_sources[file_str] = fh.read() + "\n" - - storage_layouts = OrderedDict() - if storage_layout: - for storage_file_name, contract_name in zip(storage_layout, contract_sources.keys()): - storage_file_path = Path(storage_file_name) - with storage_file_path.open() as sfh: - storage_layouts[contract_name] = json.load(sfh) + input_bundle = FilesystemInputBundle([root_path]) show_version = False if "combined_json" in output_formats: @@ -318,20 +243,44 @@ def compile_files( translate_map = {"abi_python": "abi", "json": "abi", "ast": "ast_dict", "ir_json": "ir_dict"} final_formats = [translate_map.get(i, i) for i in output_formats] - compiler_data = vyper.compile_codes( - contract_sources, - final_formats, - exc_handler=exc_handler, - interface_codes=get_interface_codes(root_path, contract_sources), - settings=settings, - storage_layouts=storage_layouts, - show_gas_estimates=show_gas_estimates, - no_bytecode_metadata=no_bytecode_metadata, - ) + if storage_layout_paths: + if len(storage_layout_paths) != len(input_files): + raise ValueError( + "provided {len(storage_layout_paths)} storage " + "layouts, but {len(input_files)} source files" + ) + + ret: dict[Any, Any] = {} if show_version: - compiler_data["version"] = vyper.__version__ + ret["version"] = vyper.__version__ - return compiler_data + for file_name in input_files: + file_path = Path(file_name) + file = input_bundle.load_file(file_path) + assert isinstance(file, FileInput) # mypy hint + + storage_layout_override = None + if storage_layout_paths: + storage_file_path = storage_layout_paths.pop(0) + with open(storage_file_path) as sfh: + storage_layout_override = json.load(sfh) + + output = vyper.compile_code( + file.source_code, + contract_name=str(file.path), + source_id=file.source_id, + input_bundle=input_bundle, + output_formats=final_formats, + exc_handler=exc_handler, + settings=settings, + storage_layout_override=storage_layout_override, + show_gas_estimates=show_gas_estimates, + no_bytecode_metadata=no_bytecode_metadata, + ) + + ret[file_path] = output + + return ret if __name__ == "__main__": diff --git a/vyper/cli/vyper_json.py b/vyper/cli/vyper_json.py index f6d82c3fe0..2720f20d23 100755 --- a/vyper/cli/vyper_json.py +++ b/vyper/cli/vyper_json.py @@ -4,15 +4,14 @@ import json import sys import warnings -from pathlib import Path -from typing import Any, Callable, Dict, Hashable, List, Optional, Tuple, Union +from pathlib import Path, PurePath +from typing import Any, Callable, Hashable, Optional import vyper -from vyper.cli.utils import extract_file_interface_imports, get_interface_file_path +from vyper.compiler.input_bundle import FileInput, JSONInputBundle from vyper.compiler.settings import OptimizationLevel, Settings from vyper.evm.opcodes import EVM_VERSIONS from vyper.exceptions import JSONError -from vyper.typing import ContractCodes, ContractPath from vyper.utils import keccak256 TRANSLATE_MAP = { @@ -97,15 +96,15 @@ def _parse_args(argv): print(output_json) -def exc_handler_raises(file_path: Union[str, None], exception: Exception, component: str) -> None: +def exc_handler_raises(file_path: Optional[str], exception: Exception, component: str) -> None: if file_path: print(f"Unhandled exception in '{file_path}':") exception._exc_handler = True # type: ignore raise exception -def exc_handler_to_dict(file_path: Union[str, None], exception: Exception, component: str) -> Dict: - err_dict: Dict = { +def exc_handler_to_dict(file_path: Optional[str], exception: Exception, component: str) -> dict: + err_dict: dict = { "type": type(exception).__name__, "component": component, "severity": "error", @@ -129,23 +128,7 @@ def exc_handler_to_dict(file_path: Union[str, None], exception: Exception, compo return output_json -def _standardize_path(path_str: str) -> str: - try: - path = Path(path_str) - - if path.is_absolute(): - path = path.resolve() - else: - pwd = Path(".").resolve() - path = path.resolve().relative_to(pwd) - - except ValueError: - raise JSONError(f"{path_str} - path exists outside base folder") - - return path.as_posix() - - -def get_evm_version(input_dict: Dict) -> Optional[str]: +def get_evm_version(input_dict: dict) -> Optional[str]: if "settings" not in input_dict: return None @@ -168,76 +151,75 @@ def get_evm_version(input_dict: Dict) -> Optional[str]: return evm_version -def get_input_dict_contracts(input_dict: Dict) -> ContractCodes: - contract_sources: ContractCodes = {} +def get_compilation_targets(input_dict: dict) -> list[PurePath]: + # TODO: once we have modules, add optional "compilation_targets" key + # which specifies which sources we actually want to compile. + + return [PurePath(p) for p in input_dict["sources"].keys()] + + +def get_inputs(input_dict: dict) -> dict[PurePath, Any]: + ret = {} + seen = {} + for path, value in input_dict["sources"].items(): + path = PurePath(path) if "urls" in value: raise JSONError(f"{path} - 'urls' is not a supported field, use 'content' instead") if "content" not in value: raise JSONError(f"{path} missing required field - 'content'") if "keccak256" in value: - hash_ = value["keccak256"].lower() - if hash_.startswith("0x"): - hash_ = hash_[2:] + hash_ = value["keccak256"].lower().removeprefix("0x") if hash_ != keccak256(value["content"].encode("utf-8")).hex(): raise JSONError( f"Calculated keccak of '{path}' does not match keccak given in input JSON" ) - key = _standardize_path(path) - if key in contract_sources: - raise JSONError(f"Contract namespace collision: {key}") - contract_sources[key] = value["content"] - return contract_sources + if path.stem in seen: + raise JSONError(f"Contract namespace collision: {path}") - -def get_input_dict_interfaces(input_dict: Dict) -> Dict: - interface_sources: Dict = {} + # value looks like {"content": } + # this will be interpreted by JSONInputBundle later + ret[path] = value + seen[path.stem] = True for path, value in input_dict.get("interfaces", {}).items(): - key = _standardize_path(path) - - if key.endswith(".json"): - # EthPM Manifest v3 (EIP-2678) - if "contractTypes" in value: - for name, ct in value["contractTypes"].items(): - if name in interface_sources: - raise JSONError(f"Interface namespace collision: {name}") - - interface_sources[name] = {"type": "json", "code": ct["abi"]} - - continue # Skip to next interface - - # ABI JSON file (`{"abi": List[ABI]}`) - elif "abi" in value: - interface = {"type": "json", "code": value["abi"]} - - # ABI JSON file (`List[ABI]`) - elif isinstance(value, list): - interface = {"type": "json", "code": value} - - else: - raise JSONError(f"Interface '{path}' must have 'abi' field") - - elif key.endswith(".vy"): - if "content" not in value: - raise JSONError(f"Interface '{path}' must have 'content' field") - - interface = {"type": "vyper", "code": value["content"]} - + path = PurePath(path) + if path.stem in seen: + raise JSONError(f"Interface namespace collision: {path}") + + if isinstance(value, list): + # backwards compatibility - straight ABI with no "abi" key. + # (should probably just reject these) + value = {"abi": value} + + # some validation + if not isinstance(value, dict): + raise JSONError("invalid interface (must be a dictionary):\n{json.dumps(value)}") + if "content" in value: + if not isinstance(value["content"], str): + raise JSONError(f"invalid 'content' (expected string):\n{json.dumps(value)}") + elif "abi" in value: + if not isinstance(value["abi"], list): + raise JSONError(f"invalid 'abi' (expected list):\n{json.dumps(value)}") else: - raise JSONError(f"Interface '{path}' must have suffix '.vy' or '.json'") - - key = key.rsplit(".", maxsplit=1)[0] - if key in interface_sources: - raise JSONError(f"Interface namespace collision: {key}") + raise JSONError( + "invalid interface (must contain either 'content' or 'abi'):\n{json.dumps(value)}" + ) + if "content" in value and "abi" in value: + raise JSONError( + "invalid interface (found both 'content' and 'abi'):\n{json.dumps(value)}" + ) - interface_sources[key] = interface + ret[path] = value + seen[path.stem] = True - return interface_sources + return ret -def get_input_dict_output_formats(input_dict: Dict, contract_sources: ContractCodes) -> Dict: - output_formats = {} +# get unique output formats for each contract, given the input_dict +# NOTE: would maybe be nice to raise on duplicated output formats +def get_output_formats(input_dict: dict, targets: list[PurePath]) -> dict[PurePath, list[str]]: + output_formats: dict[PurePath, list[str]] = {} for path, outputs in input_dict["settings"]["outputSelection"].items(): if isinstance(outputs, dict): # if outputs are given in solc json format, collapse them into a single list @@ -248,6 +230,7 @@ def get_input_dict_output_formats(input_dict: Dict, contract_sources: ContractCo for key in [i for i in ("evm", "evm.bytecode", "evm.deployedBytecode") if i in outputs]: outputs.remove(key) outputs.update([i for i in TRANSLATE_MAP if i.startswith(key)]) + if "*" in outputs: outputs = TRANSLATE_MAP.values() else: @@ -259,107 +242,23 @@ def get_input_dict_output_formats(input_dict: Dict, contract_sources: ContractCo outputs = sorted(set(outputs)) if path == "*": - output_keys = list(contract_sources.keys()) + output_paths = targets else: - output_keys = [_standardize_path(path)] - if output_keys[0] not in contract_sources: - raise JSONError(f"outputSelection references unknown contract '{output_keys[0]}'") + output_paths = [PurePath(path)] + if output_paths[0] not in targets: + raise JSONError(f"outputSelection references unknown contract '{output_paths[0]}'") - for key in output_keys: - output_formats[key] = outputs + for output_path in output_paths: + output_formats[output_path] = outputs return output_formats -def get_interface_codes( - root_path: Union[Path, None], - contract_path: ContractPath, - contract_sources: ContractCodes, - interface_sources: Dict, -) -> Dict: - interface_codes: Dict = {} - interfaces: Dict = {} - - code = contract_sources[contract_path] - interface_codes = extract_file_interface_imports(code) - for interface_name, interface_path in interface_codes.items(): - # If we know the interfaces already (e.g. EthPM Manifest file) - if interface_name in interface_sources: - interfaces[interface_name] = interface_sources[interface_name] - continue - - path = Path(contract_path).parent.joinpath(interface_path).as_posix() - keys = [_standardize_path(path)] - if not interface_path.startswith("."): - keys.append(interface_path) - - key = next((i for i in keys if i in interface_sources), None) - if key: - interfaces[interface_name] = interface_sources[key] - continue - - key = next((i + ".vy" for i in keys if i + ".vy" in contract_sources), None) - if key: - interfaces[interface_name] = {"type": "vyper", "code": contract_sources[key]} - continue - - if root_path is None: - raise FileNotFoundError(f"Cannot locate interface '{interface_path}{{.vy,.json}}'") - - parent_path = root_path.joinpath(contract_path).parent - base_paths = [parent_path] - if not interface_path.startswith("."): - base_paths.append(root_path) - elif interface_path.startswith("../") and len(Path(contract_path).parent.parts) < Path( - interface_path - ).parts.count(".."): - raise FileNotFoundError( - f"{contract_path} - Cannot perform relative import outside of base folder" - ) - - valid_path = get_interface_file_path(base_paths, interface_path) - with valid_path.open() as fh: - code = fh.read() - if valid_path.suffix == ".json": - code_dict = json.loads(code.encode()) - # EthPM Manifest v3 (EIP-2678) - if "contractTypes" in code_dict: - if interface_name not in code_dict["contractTypes"]: - raise JSONError(f"'{interface_name}' not found in '{valid_path}'") - - if "abi" not in code_dict["contractTypes"][interface_name]: - raise JSONError(f"Missing abi for '{interface_name}' in '{valid_path}'") - - abi = code_dict["contractTypes"][interface_name]["abi"] - interfaces[interface_name] = {"type": "json", "code": abi} - - # ABI JSON (`{"abi": List[ABI]}`) - elif "abi" in code_dict: - interfaces[interface_name] = {"type": "json", "code": code_dict["abi"]} - - # ABI JSON (`List[ABI]`) - elif isinstance(code_dict, list): - interfaces[interface_name] = {"type": "json", "code": code_dict} - - else: - raise JSONError(f"Unexpected type in file: '{valid_path}'") - - else: - interfaces[interface_name] = {"type": "vyper", "code": code} - - return interfaces - - def compile_from_input_dict( - input_dict: Dict, - exc_handler: Callable = exc_handler_raises, - root_folder: Union[str, None] = None, -) -> Tuple[Dict, Dict]: - root_path = None - if root_folder is not None: - root_path = Path(root_folder).resolve() - if not root_path.exists(): - raise FileNotFoundError(f"Invalid root path - '{root_path.as_posix()}' does not exist") + input_dict: dict, exc_handler: Callable = exc_handler_raises, root_folder: Optional[str] = None +) -> tuple[dict, dict]: + if root_folder is None: + root_folder = "." if input_dict["language"] != "Vyper": raise JSONError(f"Invalid language '{input_dict['language']}' - Only Vyper is supported.") @@ -382,46 +281,50 @@ def compile_from_input_dict( no_bytecode_metadata = not input_dict["settings"].get("bytecodeMetadata", True) - contract_sources: ContractCodes = get_input_dict_contracts(input_dict) - interface_sources = get_input_dict_interfaces(input_dict) - output_formats = get_input_dict_output_formats(input_dict, contract_sources) + compilation_targets = get_compilation_targets(input_dict) + sources = get_inputs(input_dict) + output_formats = get_output_formats(input_dict, compilation_targets) - compiler_data, warning_data = {}, {} + input_bundle = JSONInputBundle(sources, search_paths=[Path(root_folder)]) + + res, warnings_dict = {}, {} warnings.simplefilter("always") - for id_, contract_path in enumerate(sorted(contract_sources)): + for contract_path in compilation_targets: with warnings.catch_warnings(record=True) as caught_warnings: try: - interface_codes = get_interface_codes( - root_path, contract_path, contract_sources, interface_sources - ) - except Exception as exc: - return exc_handler(contract_path, exc, "parser"), {} - try: - data = vyper.compile_codes( - {contract_path: contract_sources[contract_path]}, - output_formats[contract_path], - interface_codes=interface_codes, - initial_id=id_, + # use load_file to get a unique source_id + file = input_bundle.load_file(contract_path) + assert isinstance(file, FileInput) # mypy hint + data = vyper.compile_code( + file.source_code, + contract_name=str(file.path), + input_bundle=input_bundle, + output_formats=output_formats[contract_path], + source_id=file.source_id, settings=settings, no_bytecode_metadata=no_bytecode_metadata, ) + assert isinstance(data, dict) + data["source_id"] = file.source_id except Exception as exc: return exc_handler(contract_path, exc, "compiler"), {} - compiler_data[contract_path] = data[contract_path] + res[contract_path] = data if caught_warnings: - warning_data[contract_path] = caught_warnings + warnings_dict[contract_path] = caught_warnings - return compiler_data, warning_data + return res, warnings_dict -def format_to_output_dict(compiler_data: Dict) -> Dict: - output_dict: Dict = {"compiler": f"vyper-{vyper.__version__}", "contracts": {}, "sources": {}} - for id_, (path, data) in enumerate(compiler_data.items()): - output_dict["sources"][path] = {"id": id_} +# convert output of compile_input_dict to final output format +def format_to_output_dict(compiler_data: dict) -> dict: + output_dict: dict = {"compiler": f"vyper-{vyper.__version__}", "contracts": {}, "sources": {}} + for path, data in compiler_data.items(): + path = str(path) # Path breaks json serializability + output_dict["sources"][path] = {"id": data["source_id"]} if "ast_dict" in data: output_dict["sources"][path]["ast"] = data["ast_dict"]["ast"] - name = Path(path).stem + name = PurePath(path).stem output_dict["contracts"][path] = {name: {}} output_contracts = output_dict["contracts"][path][name] @@ -459,7 +362,7 @@ def format_to_output_dict(compiler_data: Dict) -> Dict: # https://stackoverflow.com/a/49518779 -def _raise_on_duplicate_keys(ordered_pairs: List[Tuple[Hashable, Any]]) -> Dict: +def _raise_on_duplicate_keys(ordered_pairs: list[tuple[Hashable, Any]]) -> dict: """ Raise JSONError if a duplicate key exists in provided ordered list of pairs, otherwise return a dict. @@ -474,17 +377,15 @@ def _raise_on_duplicate_keys(ordered_pairs: List[Tuple[Hashable, Any]]) -> Dict: def compile_json( - input_json: Union[Dict, str], + input_json: dict | str, exc_handler: Callable = exc_handler_raises, - root_path: Union[str, None] = None, - json_path: Union[str, None] = None, -) -> Dict: + root_folder: Optional[str] = None, + json_path: Optional[str] = None, +) -> dict: try: if isinstance(input_json, str): try: - input_dict: Dict = json.loads( - input_json, object_pairs_hook=_raise_on_duplicate_keys - ) + input_dict = json.loads(input_json, object_pairs_hook=_raise_on_duplicate_keys) except json.decoder.JSONDecodeError as exc: new_exc = JSONError(str(exc), exc.lineno, exc.colno) return exc_handler(json_path, new_exc, "json") @@ -492,7 +393,7 @@ def compile_json( input_dict = input_json try: - compiler_data, warn_data = compile_from_input_dict(input_dict, exc_handler, root_path) + compiler_data, warn_data = compile_from_input_dict(input_dict, exc_handler, root_folder) if "errors" in compiler_data: return compiler_data except KeyError as exc: diff --git a/vyper/cli/vyper_serve.py b/vyper/cli/vyper_serve.py index 401e59e7ba..9771dc922d 100755 --- a/vyper/cli/vyper_serve.py +++ b/vyper/cli/vyper_serve.py @@ -91,11 +91,11 @@ def _compile(self, data): try: code = data["code"] - out_dict = vyper.compile_codes( - {"": code}, + out_dict = vyper.compile_code( + code, list(vyper.compiler.OUTPUT_FORMATS.keys()), evm_version=data.get("evm_version", DEFAULT_EVM_VERSION), - )[""] + ) out_dict["ir"] = str(out_dict["ir"]) out_dict["ir_runtime"] = str(out_dict["ir_runtime"]) except VyperException as e: diff --git a/vyper/compiler/__init__.py b/vyper/compiler/__init__.py index b1c4201361..62ea05b243 100644 --- a/vyper/compiler/__init__.py +++ b/vyper/compiler/__init__.py @@ -1,21 +1,15 @@ from collections import OrderedDict +from pathlib import Path from typing import Any, Callable, Dict, Optional, Sequence, Union import vyper.ast as vy_ast # break an import cycle import vyper.codegen.core as codegen import vyper.compiler.output as output +from vyper.compiler.input_bundle import InputBundle, PathLike from vyper.compiler.phases import CompilerData from vyper.compiler.settings import Settings from vyper.evm.opcodes import DEFAULT_EVM_VERSION, anchor_evm_version -from vyper.typing import ( - ContractCodes, - ContractPath, - InterfaceDict, - InterfaceImports, - OutputDict, - OutputFormats, - StorageLayout, -) +from vyper.typing import ContractPath, OutputFormats, StorageLayout OUTPUT_FORMATS = { # requires vyper_module @@ -47,119 +41,25 @@ } -def compile_codes( - contract_sources: ContractCodes, - output_formats: Union[OutputDict, OutputFormats, None] = None, - exc_handler: Union[Callable, None] = None, - interface_codes: Union[InterfaceDict, InterfaceImports, None] = None, - initial_id: int = 0, - settings: Settings = None, - storage_layouts: Optional[dict[ContractPath, Optional[StorageLayout]]] = None, - show_gas_estimates: bool = False, - no_bytecode_metadata: bool = False, -) -> OrderedDict: - """ - Generate compiler output(s) from one or more contract source codes. - - Arguments - --------- - contract_sources: Dict[str, str] - Vyper source codes to be compiled. Formatted as `{"contract name": "source code"}` - output_formats: List, optional - List of compiler outputs to generate. Possible options are all the keys - in `OUTPUT_FORMATS`. If not given, the deployment bytecode is generated. - exc_handler: Callable, optional - Callable used to handle exceptions if the compilation fails. Should accept - two arguments - the name of the contract, and the exception that was raised - initial_id: int, optional - The lowest source ID value to be used when generating the source map. - settings: Settings, optional - Compiler settings - show_gas_estimates: bool, optional - Show gas estimates for abi and ir output modes - interface_codes: Dict, optional - Interfaces that may be imported by the contracts during compilation. - - * May be a singular dictionary shared across all sources to be compiled, - i.e. `{'interface name': "definition"}` - * or may be organized according to contracts that are being compiled, i.e. - `{'contract name': {'interface name': "definition"}` - - * Interface definitions are formatted as: `{'type': "json/vyper", 'code': "interface code"}` - * JSON interfaces are given as lists, vyper interfaces as strings - no_bytecode_metadata: bool, optional - Do not add metadata to bytecode. Defaults to False - - Returns - ------- - Dict - Compiler output as `{'contract name': {'output key': "output data"}}` - """ - settings = settings or Settings() - - if output_formats is None: - output_formats = ("bytecode",) - if isinstance(output_formats, Sequence): - output_formats = dict((k, output_formats) for k in contract_sources.keys()) - - out: OrderedDict = OrderedDict() - for source_id, contract_name in enumerate(sorted(contract_sources), start=initial_id): - source_code = contract_sources[contract_name] - interfaces: Any = interface_codes - storage_layout_override = None - if storage_layouts and contract_name in storage_layouts: - storage_layout_override = storage_layouts[contract_name] - - if ( - isinstance(interfaces, dict) - and contract_name in interfaces - and isinstance(interfaces[contract_name], dict) - ): - interfaces = interfaces[contract_name] - - # make IR output the same between runs - codegen.reset_names() - - compiler_data = CompilerData( - source_code, - contract_name, - interfaces, - source_id, - settings, - storage_layout_override, - show_gas_estimates, - no_bytecode_metadata, - ) - with anchor_evm_version(compiler_data.settings.evm_version): - for output_format in output_formats[contract_name]: - if output_format not in OUTPUT_FORMATS: - raise ValueError(f"Unsupported format type {repr(output_format)}") - try: - out.setdefault(contract_name, {}) - formatter = OUTPUT_FORMATS[output_format] - out[contract_name][output_format] = formatter(compiler_data) - except Exception as exc: - if exc_handler is not None: - exc_handler(contract_name, exc) - else: - raise exc - - return out - - UNKNOWN_CONTRACT_NAME = "" def compile_code( contract_source: str, - output_formats: Optional[OutputFormats] = None, - interface_codes: Optional[InterfaceImports] = None, + contract_name: str = UNKNOWN_CONTRACT_NAME, + source_id: int = 0, + input_bundle: InputBundle = None, settings: Settings = None, + output_formats: Optional[OutputFormats] = None, storage_layout_override: Optional[StorageLayout] = None, + no_bytecode_metadata: bool = False, show_gas_estimates: bool = False, + exc_handler: Optional[Callable] = None, ) -> dict: """ - Generate compiler output(s) from a single contract source code. + Generate consumable compiler output(s) from a single contract source code. + Basically, a wrapper around CompilerData which munges the output + data into the requested output formats. Arguments --------- @@ -175,11 +75,11 @@ def compile_code( Compiler settings. show_gas_estimates: bool, optional Show gas estimates for abi and ir output modes - interface_codes: Dict, optional - Interfaces that may be imported by the contracts during compilation. - - * Formatted as as `{'interface name': {'type': "json/vyper", 'code': "interface code"}}` - * JSON interfaces are given as lists, vyper interfaces as strings + exc_handler: Callable, optional + Callable used to handle exceptions if the compilation fails. Should accept + two arguments - the name of the contract, and the exception that was raised + no_bytecode_metadata: bool, optional + Do not add metadata to bytecode. Defaults to False Returns ------- @@ -187,14 +87,37 @@ def compile_code( Compiler output as `{'output key': "output data"}` """ - contract_sources = {UNKNOWN_CONTRACT_NAME: contract_source} - storage_layouts = {UNKNOWN_CONTRACT_NAME: storage_layout_override} + settings = settings or Settings() + + if output_formats is None: + output_formats = ("bytecode",) - return compile_codes( - contract_sources, - output_formats, - interface_codes=interface_codes, - settings=settings, - storage_layouts=storage_layouts, - show_gas_estimates=show_gas_estimates, - )[UNKNOWN_CONTRACT_NAME] + # make IR output the same between runs + codegen.reset_names() + + compiler_data = CompilerData( + contract_source, + input_bundle, + Path(contract_name), + source_id, + settings, + storage_layout_override, + show_gas_estimates, + no_bytecode_metadata, + ) + + ret = {} + with anchor_evm_version(compiler_data.settings.evm_version): + for output_format in output_formats: + if output_format not in OUTPUT_FORMATS: + raise ValueError(f"Unsupported format type {repr(output_format)}") + try: + formatter = OUTPUT_FORMATS[output_format] + ret[output_format] = formatter(compiler_data) + except Exception as exc: + if exc_handler is not None: + exc_handler(contract_name, exc) + else: + raise exc + + return ret diff --git a/vyper/compiler/input_bundle.py b/vyper/compiler/input_bundle.py new file mode 100644 index 0000000000..1e41c3f137 --- /dev/null +++ b/vyper/compiler/input_bundle.py @@ -0,0 +1,180 @@ +import contextlib +import json +import os +from dataclasses import dataclass +from pathlib import Path, PurePath +from typing import Any, Iterator, Optional + +from vyper.exceptions import JSONError + +# a type to make mypy happy +PathLike = Path | PurePath + + +@dataclass +class CompilerInput: + # an input to the compiler, basically an abstraction for file contents + source_id: int + path: PathLike + + @staticmethod + def from_string(source_id: int, path: PathLike, file_contents: str) -> "CompilerInput": + try: + s = json.loads(file_contents) + return ABIInput(source_id, path, s) + except (ValueError, TypeError): + return FileInput(source_id, path, file_contents) + + +@dataclass +class FileInput(CompilerInput): + source_code: str + + +@dataclass +class ABIInput(CompilerInput): + # some json input, which has already been parsed into a dict or list + # this is needed because json inputs present json interfaces as json + # objects, not as strings. this class helps us avoid round-tripping + # back to a string to pretend it's a file. + abi: Any # something that json.load() returns + + +class _NotFound(Exception): + pass + + +# wrap os.path.normpath, but return the same type as the input +def _normpath(path): + return path.__class__(os.path.normpath(path)) + + +# an "input bundle" to the compiler, representing the files which are +# available to the compiler. it is useful because it parametrizes I/O +# operations over different possible input types. you can think of it +# as a virtual filesystem which models the compiler's interactions +# with the outside world. it exposes a "load_file" operation which +# searches for a file from a set of search paths, and also provides +# id generation service to get a unique source id per file. +class InputBundle: + # a list of search paths + search_paths: list[PathLike] + + def __init__(self, search_paths): + self.search_paths = search_paths + self._source_id_counter = 0 + self._source_ids: dict[PathLike, int] = {} + + def _load_from_path(self, path): + raise NotImplementedError(f"not implemented! {self.__class__}._load_from_path()") + + def _generate_source_id(self, path: PathLike) -> int: + if path not in self._source_ids: + self._source_ids[path] = self._source_id_counter + self._source_id_counter += 1 + + return self._source_ids[path] + + def load_file(self, path: PathLike | str) -> CompilerInput: + # search path precedence + tried = [] + for sp in reversed(self.search_paths): + # note from pathlib docs: + # > If the argument is an absolute path, the previous path is ignored. + # Path("/a") / Path("/b") => Path("/b") + to_try = sp / path + + # normalize the path with os.path.normpath, to break down + # things like "foo/bar/../x.vy" => "foo/x.vy", with all + # the caveats around symlinks that os.path.normpath comes with. + to_try = _normpath(to_try) + try: + res = self._load_from_path(to_try) + break + except _NotFound: + tried.append(to_try) + + else: + formatted_search_paths = "\n".join([" " + str(p) for p in tried]) + raise FileNotFoundError( + f"could not find {path} in any of the following locations:\n" + f"{formatted_search_paths}" + ) + + # try to parse from json, so that return types are consistent + # across FilesystemInputBundle and JSONInputBundle. + if isinstance(res, FileInput): + return CompilerInput.from_string(res.source_id, res.path, res.source_code) + + return res + + def add_search_path(self, path: PathLike) -> None: + self.search_paths.append(path) + + # temporarily add something to the search path (within the + # scope of the context manager) with highest precedence. + # if `path` is None, do nothing + @contextlib.contextmanager + def search_path(self, path: Optional[PathLike]) -> Iterator[None]: + if path is None: + yield # convenience, so caller does not have to handle null path + + else: + self.search_paths.append(path) + try: + yield + finally: + self.search_paths.pop() + + +# regular input. takes a search path(s), and `load_file()` will search all +# search paths for the file and read it from the filesystem +class FilesystemInputBundle(InputBundle): + def _load_from_path(self, path: Path) -> CompilerInput: + try: + with path.open() as f: + code = f.read() + except FileNotFoundError: + raise _NotFound(path) + + source_id = super()._generate_source_id(path) + + return FileInput(source_id, path, code) + + +# fake filesystem for JSON inputs. takes a base path, and `load_file()` +# "reads" the file from the JSON input. Note that this input bundle type +# never actually interacts with the filesystem -- it is guaranteed to be pure! +class JSONInputBundle(InputBundle): + input_json: dict[PurePath, Any] + + def __init__(self, input_json, search_paths): + super().__init__(search_paths) + self.input_json = {} + for path, item in input_json.items(): + path = _normpath(path) + + # should be checked by caller + assert path not in self.input_json + self.input_json[_normpath(path)] = item + + def _load_from_path(self, path: PurePath) -> CompilerInput: + try: + value = self.input_json[path] + except KeyError: + raise _NotFound(path) + + source_id = super()._generate_source_id(path) + + if "content" in value: + return FileInput(source_id, path, value["content"]) + + if "abi" in value: + return ABIInput(source_id, path, value["abi"]) + + # TODO: ethPM support + # if isinstance(contents, dict) and "contractTypes" in contents: + + # unreachable, based on how JSONInputBundle is constructed in + # the codebase. + raise JSONError(f"Unexpected type in file: '{path}'") # pragma: nocover diff --git a/vyper/compiler/output.py b/vyper/compiler/output.py index 1c38fcff9b..e47f300ba9 100644 --- a/vyper/compiler/output.py +++ b/vyper/compiler/output.py @@ -1,6 +1,5 @@ import warnings from collections import OrderedDict, deque -from pathlib import Path import asttokens @@ -17,7 +16,7 @@ def build_ast_dict(compiler_data: CompilerData) -> dict: ast_dict = { - "contract_name": compiler_data.contract_name, + "contract_name": str(compiler_data.contract_path), "ast": ast_to_dict(compiler_data.vyper_module), } return ast_dict @@ -35,7 +34,7 @@ def build_userdoc(compiler_data: CompilerData) -> dict: def build_external_interface_output(compiler_data: CompilerData) -> str: interface = compiler_data.vyper_module_folded._metadata["type"] - stem = Path(compiler_data.contract_name).stem + stem = compiler_data.contract_path.stem # capitalize words separated by '_' # ex: test_interface.vy -> TestInterface name = "".join([x.capitalize() for x in stem.split("_")]) diff --git a/vyper/compiler/phases.py b/vyper/compiler/phases.py index 72be4396e4..bfbb336d54 100644 --- a/vyper/compiler/phases.py +++ b/vyper/compiler/phases.py @@ -1,6 +1,7 @@ import copy import warnings from functools import cached_property +from pathlib import Path, PurePath from typing import Optional, Tuple from vyper import ast as vy_ast @@ -8,12 +9,15 @@ from vyper.codegen.core import anchor_opt_level from vyper.codegen.global_context import GlobalContext from vyper.codegen.ir_node import IRnode +from vyper.compiler.input_bundle import FilesystemInputBundle, InputBundle from vyper.compiler.settings import OptimizationLevel, Settings from vyper.exceptions import StructureException from vyper.ir import compile_ir, optimizer from vyper.semantics import set_data_positions, validate_semantics from vyper.semantics.types.function import ContractFunctionT -from vyper.typing import InterfaceImports, StorageLayout +from vyper.typing import StorageLayout + +DEFAULT_CONTRACT_NAME = PurePath("VyperContract.vy") class CompilerData: @@ -49,8 +53,8 @@ class CompilerData: def __init__( self, source_code: str, - contract_name: str = "VyperContract", - interface_codes: Optional[InterfaceImports] = None, + input_bundle: InputBundle = None, + contract_path: Path | PurePath = DEFAULT_CONTRACT_NAME, source_id: int = 0, settings: Settings = None, storage_layout: StorageLayout = None, @@ -62,15 +66,11 @@ def __init__( Arguments --------- - source_code : str + source_code: str Vyper source code. - contract_name : str, optional + contract_path: Path, optional The name of the contract being compiled. - interface_codes: Dict, optional - Interfaces that may be imported by the contracts during compilation. - * Formatted as as `{'interface name': {'type': "json/vyper", 'code': "interface code"}}` - * JSON interfaces are given as lists, vyper interfaces as strings - source_id : int, optional + source_id: int, optional ID number used to identify this contract in the source map. settings: Settings Set optimization mode. @@ -79,20 +79,22 @@ def __init__( no_bytecode_metadata: bool, optional Do not add metadata to bytecode. Defaults to False """ - self.contract_name = contract_name + self.contract_path = contract_path self.source_code = source_code - self.interface_codes = interface_codes self.source_id = source_id self.storage_layout_override = storage_layout self.show_gas_estimates = show_gas_estimates self.no_bytecode_metadata = no_bytecode_metadata + self.settings = settings or Settings() + self.input_bundle = input_bundle or FilesystemInputBundle([Path(".")]) _ = self._generate_ast # force settings to be calculated @cached_property def _generate_ast(self): - settings, ast = generate_ast(self.source_code, self.source_id, self.contract_name) + contract_name = str(self.contract_path) + settings, ast = generate_ast(self.source_code, self.source_id, contract_name) # validate the compiler settings # XXX: this is a bit ugly, clean up later @@ -133,12 +135,12 @@ def vyper_module_unfolded(self) -> vy_ast.Module: # This phase is intended to generate an AST for tooling use, and is not # used in the compilation process. - return generate_unfolded_ast(self.vyper_module, self.interface_codes) + return generate_unfolded_ast(self.contract_path, self.vyper_module, self.input_bundle) @cached_property def _folded_module(self): return generate_folded_ast( - self.vyper_module, self.interface_codes, self.storage_layout_override + self.contract_path, self.vyper_module, self.input_bundle, self.storage_layout_override ) @property @@ -220,7 +222,7 @@ def generate_ast( Vyper source code. source_id : int ID number used to identify this contract in the source map. - contract_name : str + contract_name: str Name of the contract. Returns @@ -231,20 +233,24 @@ def generate_ast( return vy_ast.parse_to_ast_with_settings(source_code, source_id, contract_name) +# destructive -- mutates module in place! def generate_unfolded_ast( - vyper_module: vy_ast.Module, interface_codes: Optional[InterfaceImports] + contract_path: Path | PurePath, vyper_module: vy_ast.Module, input_bundle: InputBundle ) -> vy_ast.Module: vy_ast.validation.validate_literal_nodes(vyper_module) vy_ast.folding.replace_builtin_functions(vyper_module) - # note: validate_semantics does type inference on the AST - validate_semantics(vyper_module, interface_codes) + + with input_bundle.search_path(contract_path.parent): + # note: validate_semantics does type inference on the AST + validate_semantics(vyper_module, input_bundle) return vyper_module def generate_folded_ast( + contract_path: Path, vyper_module: vy_ast.Module, - interface_codes: Optional[InterfaceImports], + input_bundle: InputBundle, storage_layout_overrides: StorageLayout = None, ) -> Tuple[vy_ast.Module, StorageLayout]: """ @@ -262,11 +268,15 @@ def generate_folded_ast( StorageLayout Layout of variables in storage """ + vy_ast.validation.validate_literal_nodes(vyper_module) vyper_module_folded = copy.deepcopy(vyper_module) vy_ast.folding.fold(vyper_module_folded) - validate_semantics(vyper_module_folded, interface_codes) + + with input_bundle.search_path(contract_path.parent): + validate_semantics(vyper_module_folded, input_bundle) + symbol_tables = set_data_positions(vyper_module_folded, storage_layout_overrides) return vyper_module_folded, symbol_tables diff --git a/vyper/semantics/analysis/__init__.py b/vyper/semantics/analysis/__init__.py index 9e987d1cd0..7db230167e 100644 --- a/vyper/semantics/analysis/__init__.py +++ b/vyper/semantics/analysis/__init__.py @@ -7,11 +7,11 @@ from .utils import _ExprAnalyser -def validate_semantics(vyper_ast, interface_codes): +def validate_semantics(vyper_ast, input_bundle): # validate semantics and annotate AST with type/semantics information namespace = get_namespace() with namespace.enter_scope(): - add_module_namespace(vyper_ast, interface_codes) + add_module_namespace(vyper_ast, input_bundle) vy_ast.expansion.expand_annotated_ast(vyper_ast) validate_functions(vyper_ast) diff --git a/vyper/semantics/analysis/module.py b/vyper/semantics/analysis/module.py index e59422294c..239438f35b 100644 --- a/vyper/semantics/analysis/module.py +++ b/vyper/semantics/analysis/module.py @@ -1,13 +1,13 @@ -import importlib -import pkgutil -from typing import Optional, Union +import os +from pathlib import Path, PurePath +from typing import Optional import vyper.builtins.interfaces from vyper import ast as vy_ast +from vyper.compiler.input_bundle import ABIInput, FileInput, FilesystemInputBundle, InputBundle from vyper.evm.opcodes import version_check from vyper.exceptions import ( CallViolation, - CompilerPanic, ExceptionList, InvalidLiteral, InvalidType, @@ -15,30 +15,27 @@ StateAccessViolation, StructureException, SyntaxException, - UndeclaredDefinition, VariableDeclarationException, VyperException, ) from vyper.semantics.analysis.base import VarInfo from vyper.semantics.analysis.common import VyperNodeVisitorBase -from vyper.semantics.analysis.levenshtein_utils import get_levenshtein_error_suggestions from vyper.semantics.analysis.utils import check_constant, validate_expected_type from vyper.semantics.data_locations import DataLocation from vyper.semantics.namespace import Namespace, get_namespace from vyper.semantics.types import EnumT, EventT, InterfaceT, StructT from vyper.semantics.types.function import ContractFunctionT from vyper.semantics.types.utils import type_from_annotation -from vyper.typing import InterfaceDict -def add_module_namespace(vy_module: vy_ast.Module, interface_codes: InterfaceDict) -> None: +def add_module_namespace(vy_module: vy_ast.Module, input_bundle: InputBundle) -> None: """ Analyze a Vyper module AST node, add all module-level objects to the namespace and validate top-level correctness """ namespace = get_namespace() - ModuleAnalyzer(vy_module, interface_codes, namespace) + ModuleAnalyzer(vy_module, input_bundle, namespace) def _find_cyclic_call(fn_names: list, self_members: dict) -> Optional[list]: @@ -58,10 +55,10 @@ class ModuleAnalyzer(VyperNodeVisitorBase): scope_name = "module" def __init__( - self, module_node: vy_ast.Module, interface_codes: InterfaceDict, namespace: Namespace + self, module_node: vy_ast.Module, input_bundle: InputBundle, namespace: Namespace ) -> None: self.ast = module_node - self.interface_codes = interface_codes or {} + self.input_bundle = input_bundle self.namespace = namespace # TODO: Move computation out of constructor @@ -287,17 +284,19 @@ def visit_FunctionDef(self, node): def visit_Import(self, node): if not node.alias: raise StructureException("Import requires an accompanying `as` statement", node) - _add_import(node, node.name, node.alias, node.alias, self.interface_codes, self.namespace) + # import x.y[name] as y[alias] + self._add_import(node, 0, node.name, node.alias) def visit_ImportFrom(self, node): - _add_import( - node, - node.module, - node.name, - node.alias or node.name, - self.interface_codes, - self.namespace, - ) + # from m.n[module] import x[name] as y[alias] + alias = node.alias or node.name + + module = node.module or "" + if module: + module += "." + + qualified_module_name = module + node.name + self._add_import(node, node.level, qualified_module_name, alias) def visit_InterfaceDef(self, node): obj = InterfaceT.from_ast(node) @@ -313,41 +312,87 @@ def visit_StructDef(self, node): except VyperException as exc: raise exc.with_annotation(node) from None + def _add_import( + self, node: vy_ast.VyperNode, level: int, qualified_module_name: str, alias: str + ) -> None: + type_ = self._load_import(level, qualified_module_name) + + try: + self.namespace[alias] = type_ + except VyperException as exc: + raise exc.with_annotation(node) from None + + # load an InterfaceT from an import. + # raises FileNotFoundError + def _load_import(self, level: int, module_str: str) -> InterfaceT: + if _is_builtin(module_str): + return _load_builtin_import(level, module_str) + + path = _import_to_path(level, module_str) + + try: + file = self.input_bundle.load_file(path.with_suffix(".vy")) + assert isinstance(file, FileInput) # mypy hint + interface_ast = vy_ast.parse_to_ast(file.source_code, contract_name=str(file.path)) + return InterfaceT.from_ast(interface_ast) + except FileNotFoundError: + pass + + try: + file = self.input_bundle.load_file(path.with_suffix(".json")) + assert isinstance(file, ABIInput) # mypy hint + return InterfaceT.from_json_abi(str(file.path), file.abi) + except FileNotFoundError: + raise ModuleNotFoundError(module_str) + + +# convert an import to a path (without suffix) +def _import_to_path(level: int, module_str: str) -> PurePath: + base_path = "" + if level > 1: + base_path = "../" * (level - 1) + elif level == 1: + base_path = "./" + return PurePath(f"{base_path}{module_str.replace('.','/')}/") + + +# can add more, e.g. "vyper.builtins.interfaces", etc. +BUILTIN_PREFIXES = ["vyper.interfaces"] + + +def _is_builtin(module_str): + return any(module_str.startswith(prefix) for prefix in BUILTIN_PREFIXES) + + +def _load_builtin_import(level: int, module_str: str) -> InterfaceT: + if not _is_builtin(module_str): + raise ModuleNotFoundError(f"Not a builtin: {module_str}") from None + + builtins_path = vyper.builtins.interfaces.__path__[0] + # hygiene: convert to relpath to avoid leaking user directory info + # (note Path.relative_to cannot handle absolute to relative path + # conversion, so we must use the `os` module). + builtins_path = os.path.relpath(builtins_path) + + search_path = Path(builtins_path).parent.parent.parent + # generate an input bundle just because it knows how to build paths. + input_bundle = FilesystemInputBundle([search_path]) + + # remap builtins directory -- + # vyper/interfaces => vyper/builtins/interfaces + remapped_module = module_str + if remapped_module.startswith("vyper.interfaces"): + remapped_module = remapped_module.removeprefix("vyper.interfaces") + remapped_module = vyper.builtins.interfaces.__package__ + remapped_module -def _add_import( - node: Union[vy_ast.Import, vy_ast.ImportFrom], - module: str, - name: str, - alias: str, - interface_codes: InterfaceDict, - namespace: dict, -) -> None: - if module == "vyper.interfaces": - interface_codes = _get_builtin_interfaces() - if name not in interface_codes: - suggestions_str = get_levenshtein_error_suggestions(name, _get_builtin_interfaces(), 1.0) - raise UndeclaredDefinition(f"Unknown interface: {name}. {suggestions_str}", node) - - if interface_codes[name]["type"] == "vyper": - interface_ast = vy_ast.parse_to_ast(interface_codes[name]["code"], contract_name=name) - type_ = InterfaceT.from_ast(interface_ast) - elif interface_codes[name]["type"] == "json": - type_ = InterfaceT.from_json_abi(name, interface_codes[name]["code"]) # type: ignore - else: - raise CompilerPanic(f"Unknown interface format: {interface_codes[name]['type']}") + path = _import_to_path(level, remapped_module).with_suffix(".vy") try: - namespace[alias] = type_ - except VyperException as exc: - raise exc.with_annotation(node) from None - - -def _get_builtin_interfaces(): - interface_names = [i.name for i in pkgutil.iter_modules(vyper.builtins.interfaces.__path__)] - return { - name: { - "type": "vyper", - "code": importlib.import_module(f"vyper.builtins.interfaces.{name}").interface_code, - } - for name in interface_names - } + file = input_bundle.load_file(path) + assert isinstance(file, FileInput) # mypy hint + except FileNotFoundError: + raise ModuleNotFoundError(f"Not a builtin: {module_str}") from None + + # TODO: it might be good to cache this computation + interface_ast = vy_ast.parse_to_ast(file.source_code, contract_name=module_str) + return InterfaceT.from_ast(interface_ast) diff --git a/vyper/typing.py b/vyper/typing.py index 18e201e814..ad3964dff9 100644 --- a/vyper/typing.py +++ b/vyper/typing.py @@ -7,17 +7,9 @@ # Compiler ContractPath = str SourceCode = str -ContractCodes = Dict[ContractPath, SourceCode] OutputFormats = Sequence[str] -OutputDict = Dict[ContractPath, OutputFormats] StorageLayout = Dict -# Interfaces -InterfaceAsName = str -InterfaceImportPath = str -InterfaceImports = Dict[InterfaceAsName, InterfaceImportPath] -InterfaceDict = Dict[ContractPath, InterfaceImports] - # Opcodes OpcodeGasCost = Union[int, Tuple] OpcodeValue = Tuple[Optional[int], int, int, OpcodeGasCost]