From b0ea5b6f1c8cd8d09db6f37e9857f9b3837fb386 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 16 Dec 2023 13:42:31 -0500 Subject: [PATCH] feat: search path resolution for cli (#3694) the current behavior is that the current directory does *not* get into the search path when `-p` is specified, which is annoying. (one would expect `vyper some/directory/some/file.vy` to compile no matter what `-p` is specified as). this commit also handles the addition of multiple search paths specified on the CLI, and adds a long `--path` option as an alternative to `-p`. --- .../cli/vyper_compile/test_compile_files.py | 36 ++++++++++++------- tests/unit/compiler/test_input_bundle.py | 13 +------ tests/utils.py | 12 +++++++ vyper/cli/vyper_compile.py | 19 ++++++---- 4 files changed, 49 insertions(+), 31 deletions(-) create mode 100644 tests/utils.py diff --git a/tests/unit/cli/vyper_compile/test_compile_files.py b/tests/unit/cli/vyper_compile/test_compile_files.py index f6e3a51a4b..2a65d66835 100644 --- a/tests/unit/cli/vyper_compile/test_compile_files.py +++ b/tests/unit/cli/vyper_compile/test_compile_files.py @@ -2,6 +2,7 @@ import pytest +from tests.utils import working_directory from vyper.cli.vyper_compile import compile_files @@ -19,7 +20,7 @@ def test_combined_json_keys(tmp_path, make_file): "userdoc", "devdoc", } - compile_data = compile_files(["bar.vy"], ["combined_json"], root_folder=tmp_path) + compile_data = compile_files(["bar.vy"], ["combined_json"], paths=[tmp_path]) assert set(compile_data.keys()) == {Path("bar.vy"), "version"} assert set(compile_data[Path("bar.vy")].keys()) == combined_keys @@ -27,7 +28,7 @@ def test_combined_json_keys(tmp_path, make_file): def test_invalid_root_path(): with pytest.raises(FileNotFoundError): - compile_files([], [], root_folder="path/that/does/not/exist") + compile_files([], [], paths=["path/that/does/not/exist"]) CONTRACT_CODE = """ @@ -74,7 +75,7 @@ def test_import_same_folder(import_stmt, alias, tmp_path, make_file): make_file("contracts/foo.vy", CONTRACT_CODE.format(import_stmt=import_stmt, alias=alias)) make_file("contracts/IFoo.vyi", INTERFACE_CODE) - assert compile_files([foo], ["combined_json"], root_folder=tmp_path) + assert compile_files([foo], ["combined_json"], paths=[tmp_path]) SUBFOLDER_IMPORT_STMT = [ @@ -98,7 +99,7 @@ def test_import_subfolder(import_stmt, alias, tmp_path, make_file): ) make_file("contracts/other/IFoo.vyi", INTERFACE_CODE) - assert compile_files([foo], ["combined_json"], root_folder=tmp_path) + assert compile_files([foo], ["combined_json"], paths=[tmp_path]) OTHER_FOLDER_IMPORT_STMT = [ @@ -115,7 +116,7 @@ def test_import_other_folder(import_stmt, alias, tmp_path, make_file): foo = make_file("contracts/foo.vy", CONTRACT_CODE.format(import_stmt=import_stmt, alias=alias)) make_file("interfaces/IFoo.vyi", INTERFACE_CODE) - assert compile_files([foo], ["combined_json"], root_folder=tmp_path) + assert compile_files([foo], ["combined_json"], paths=[tmp_path]) def test_import_parent_folder(tmp_path, make_file): @@ -125,10 +126,21 @@ def test_import_parent_folder(tmp_path, make_file): ) make_file("IFoo.vyi", INTERFACE_CODE) - assert compile_files([foo], ["combined_json"], root_folder=tmp_path) + assert compile_files([foo], ["combined_json"], paths=[tmp_path]) # perform relative import outside of base folder - compile_files([foo], ["combined_json"], root_folder=tmp_path / "contracts") + compile_files([foo], ["combined_json"], paths=[tmp_path / "contracts"]) + + +def test_import_search_paths(tmp_path, make_file): + with working_directory(tmp_path): + contract_code = CONTRACT_CODE.format(import_stmt="from utils import IFoo", alias="IFoo") + contract_filename = "dir1/baz/foo.vy" + interface_filename = "dir2/utils/IFoo.vyi" + make_file(interface_filename, INTERFACE_CODE) + make_file(contract_filename, contract_code) + + assert compile_files([contract_filename], ["combined_json"], paths=["dir2"]) META_IMPORT_STMT = [ @@ -167,7 +179,7 @@ def be_known() -> ISelf.FooStruct: make_file("contracts/ISelf.vyi", interface_code) meta = make_file("contracts/Self.vy", code) - assert compile_files([meta], ["combined_json"], root_folder=tmp_path) + assert compile_files([meta], ["combined_json"], paths=[tmp_path]) # implement IFoo in another contract for fun @@ -187,7 +199,7 @@ def bar(_foo: address) -> {alias}.FooStruct: make_file("contracts/IFoo.vyi", INTERFACE_CODE) baz = make_file("contracts/Baz.vy", baz_code) - assert compile_files([baz], ["combined_json"], root_folder=tmp_path) + assert compile_files([baz], ["combined_json"], paths=[tmp_path]) def test_local_namespace(make_file, tmp_path): @@ -215,7 +227,7 @@ def test_local_namespace(make_file, tmp_path): for file_name in ("foo.vyi", "bar.vyi"): make_file(file_name, INTERFACE_CODE) - assert compile_files(paths, ["combined_json"], root_folder=tmp_path) + assert compile_files(paths, ["combined_json"], paths=[tmp_path]) def test_compile_outside_root_path(tmp_path, make_file): @@ -223,7 +235,7 @@ def test_compile_outside_root_path(tmp_path, make_file): make_file("ifoo.vyi", INTERFACE_CODE) foo = make_file("foo.vy", CONTRACT_CODE.format(import_stmt="import ifoo as IFoo", alias="IFoo")) - assert compile_files([foo], ["combined_json"], root_folder=".") + assert compile_files([foo], ["combined_json"], paths=None) def test_import_library(tmp_path, make_file): @@ -244,4 +256,4 @@ def foo() -> uint256: make_file("lib.vy", library_source) contract_file = make_file("contract.vy", contract_source) - assert compile_files([contract_file], ["combined_json"], root_folder=tmp_path) is not None + assert compile_files([contract_file], ["combined_json"], paths=[tmp_path]) is not None diff --git a/tests/unit/compiler/test_input_bundle.py b/tests/unit/compiler/test_input_bundle.py index e26555b169..621b529722 100644 --- a/tests/unit/compiler/test_input_bundle.py +++ b/tests/unit/compiler/test_input_bundle.py @@ -1,10 +1,9 @@ -import contextlib import json -import os from pathlib import Path, PurePath import pytest +from tests.utils import working_directory from vyper.compiler.input_bundle import ABIInput, FileInput, FilesystemInputBundle, JSONInputBundle @@ -83,16 +82,6 @@ def test_load_abi(make_file, input_bundle, tmp_path): assert file == ABIInput(1, "foo.txt", path, "some string") -@contextlib.contextmanager -def working_directory(directory): - tmp = os.getcwd() - try: - os.chdir(directory) - yield - finally: - os.chdir(tmp) - - # check that unique paths give unique source ids def test_source_id_file_input(make_file, input_bundle, tmp_path): foopath = make_file("foo.vy", "contents") diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000000..0c89c39ff3 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,12 @@ +import contextlib +import os + + +@contextlib.contextmanager +def working_directory(directory): + tmp = os.getcwd() + try: + os.chdir(directory) + yield + finally: + os.chdir(tmp) diff --git a/vyper/cli/vyper_compile.py b/vyper/cli/vyper_compile.py index ec4681a814..25f1180098 100755 --- a/vyper/cli/vyper_compile.py +++ b/vyper/cli/vyper_compile.py @@ -140,7 +140,7 @@ def _parse_args(argv): ) parser.add_argument("--hex-ir", action="store_true") parser.add_argument( - "-p", help="Set the root path for contract imports", default=".", dest="root_folder" + "--path", "-p", help="Set the root path for contract imports", action="append", dest="paths" ) parser.add_argument("-o", help="Set the output path", dest="output_path") parser.add_argument( @@ -190,7 +190,7 @@ def _parse_args(argv): compiled = compile_files( args.input_files, output_formats, - args.root_folder, + args.paths, args.show_gas_estimates, settings, args.storage_layout, @@ -228,18 +228,23 @@ def exc_handler(contract_path: ContractPath, exception: Exception) -> None: def compile_files( input_files: list[str], output_formats: OutputFormats, - root_folder: str = ".", + paths: list[str] = None, show_gas_estimates: bool = False, settings: Optional[Settings] = None, storage_layout_paths: list[str] = None, no_bytecode_metadata: bool = False, experimental_codegen: bool = False, ) -> 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") + paths = paths or [] - input_bundle = FilesystemInputBundle([root_path]) + # lowest precedence search path is always `.` + search_paths = [Path(".")] + + for p in paths: + path = Path(p).resolve(strict=True) + search_paths.append(path) + + input_bundle = FilesystemInputBundle(search_paths) show_version = False if "combined_json" in output_formats: