From faeac7ffc618c1b8788f204442a5929b32237093 Mon Sep 17 00:00:00 2001 From: Juliya Smith Date: Mon, 9 Dec 2024 14:46:05 -0600 Subject: [PATCH 1/2] feat: project flag on test cmd --- src/ape/cli/options.py | 13 ++++++++++++- src/ape/managers/project.py | 31 ++++++++++++++++++++++++++++++ src/ape_test/_cli.py | 5 +++-- tests/functional/test_project.py | 11 +++++++++++ tests/integration/cli/test_test.py | 25 ++++++++++++++++++++++++ 5 files changed, 82 insertions(+), 3 deletions(-) diff --git a/src/ape/cli/options.py b/src/ape/cli/options.py index 1c492e113f..e4a717ea04 100644 --- a/src/ape/cli/options.py +++ b/src/ape/cli/options.py @@ -573,10 +573,21 @@ def project_option(**kwargs): if (isinstance(_type, type) and issubclass(_type, Path)) else _project_callback ) + cd = kwargs.pop("cd", False) + + def callback_wrapper(ctx, param, val): + result = callback(ctx, param, val) + if cd: + # The CLI requested we also change directories into this path. + path = getattr(result, "path", result) # project.path or path + ctx.obj.local_project.chdir(path.expanduser().resolve()) + + return result + return click.option( "--project", help="The path to a local project or manifest", - callback=callback, + callback=callback_wrapper, metavar="PATH", is_eager=True, **kwargs, diff --git a/src/ape/managers/project.py b/src/ape/managers/project.py index 203619d752..d1536a5ab1 100644 --- a/src/ape/managers/project.py +++ b/src/ape/managers/project.py @@ -1,4 +1,5 @@ import json +import os import random import shutil from collections.abc import Callable, Iterable, Iterator @@ -2568,6 +2569,36 @@ def clean(self): self.sources._path_cache = None self._clear_cached_config() + def chdir(self, path: Path): + """ + Change the local project to the new path. + + Args: + path (Path): The path of the new project. + """ + if self.path == path: + return # Already there! + + os.chdir(path) + + # Clear cached properties. + for prop in ( + "path", + "_deduced_contracts_folder", + "project_api", + "contracts", + "interfaces_folder", + "sources", + ): + self.__dict__.pop(prop, None) + + # Re-initialize + self._session_source_change_check = set() + self._config_override = {} + self._base_path = Path(path).resolve() + self.manifest_path = self._base_path / ".build" / "__local__.json" + self._manifest = self.load_manifest() + def reload_config(self): """ Reload the local ape-config.yaml file. diff --git a/src/ape_test/_cli.py b/src/ape_test/_cli.py index a98baaffd5..fedb4b3b7e 100644 --- a/src/ape_test/_cli.py +++ b/src/ape_test/_cli.py @@ -7,7 +7,7 @@ import pytest from click import Command -from ape.cli.options import ape_cli_context +from ape.cli.options import ape_cli_context, project_option from ape.logging import LogLevel, _get_level @@ -79,6 +79,7 @@ def parse_args(self, ctx, args: list[str]) -> list[str]: @ape_cli_context( default_log_level=LogLevel.WARNING.value, ) +@project_option(type=Path, cd=True) @click.option( "-w", "--watch", @@ -103,7 +104,7 @@ def parse_args(self, ctx, args: list[str]) -> list[str]: help="Delay between polling cycles for `ape test --watch`. Defaults to 0.5 seconds.", ) @click.argument("pytest_args", nargs=-1, type=click.UNPROCESSED) -def cli(cli_ctx, watch, watch_folders, watch_delay, pytest_args): +def cli(cli_ctx, project, watch, watch_folders, watch_delay, pytest_args): pytest_arg_ls = [*pytest_args] if pytest_verbosity := cli_ctx.get("pytest_verbosity"): pytest_arg_ls.append(pytest_verbosity) diff --git a/tests/functional/test_project.py b/tests/functional/test_project.py index 9dc31a7db4..1b794d87c4 100644 --- a/tests/functional/test_project.py +++ b/tests/functional/test_project.py @@ -1053,3 +1053,14 @@ def test_instance_map(self, project, vyper_contract_instance, mock_sepolia): return assert False, "Failed to find expected URI" + + +def test_chdir(project): + original_path = project.path + with create_tempdir() as new_path: + project.chdir(new_path) + assert project.path == new_path + + # Undo. + project.chdir(original_path) + assert project.path == original_path diff --git a/tests/integration/cli/test_test.py b/tests/integration/cli/test_test.py index 90cf0b184c..51343353aa 100644 --- a/tests/integration/cli/test_test.py +++ b/tests/integration/cli/test_test.py @@ -442,3 +442,28 @@ def test_watch(mocker, integ_project, runner, ape_cli): assert result.exit_code == 0 runner_patch.assert_called_once_with((Path("contracts"), Path("tests")), 0.5, "-s") + + +@skip_projects_except("with-contracts") +def test_project_option(integ_project, ape_cli, runner): + _ = integ_project # NOTE: Not actually used, but avoid running across all projects. + + # NOTE: Using isolated filesystem so that + with runner.isolated_filesystem(): + # Setup a project + project_name = "test-token-project" + project_dir = Path.cwd() / project_name + tests_dir = project_dir / "tests" + tests_dir.mkdir(parents=True) + + # Setup a test + test_file = tests_dir / "test_project_option.py" + test_text = """ +def test_project_option(): + assert True +""".lstrip() + test_file.write_text(test_text) + + result = runner.invoke(ape_cli, ("test", "--project", f"./{project_name}")) + assert "1 passed" in result.output + assert result.exit_code == 0 From 126f9e11a962f38a652efd6f4ee7a7e051dca674 Mon Sep 17 00:00:00 2001 From: Juliya Smith Date: Mon, 9 Dec 2024 14:56:33 -0600 Subject: [PATCH 2/2] fix: easier sol and make work pytest --- src/ape/cli/options.py | 13 +------------ src/ape/pytest/plugin.py | 4 ++++ src/ape_test/_cli.py | 5 ++--- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/ape/cli/options.py b/src/ape/cli/options.py index e4a717ea04..1c492e113f 100644 --- a/src/ape/cli/options.py +++ b/src/ape/cli/options.py @@ -573,21 +573,10 @@ def project_option(**kwargs): if (isinstance(_type, type) and issubclass(_type, Path)) else _project_callback ) - cd = kwargs.pop("cd", False) - - def callback_wrapper(ctx, param, val): - result = callback(ctx, param, val) - if cd: - # The CLI requested we also change directories into this path. - path = getattr(result, "path", result) # project.path or path - ctx.obj.local_project.chdir(path.expanduser().resolve()) - - return result - return click.option( "--project", help="The path to a local project or manifest", - callback=callback_wrapper, + callback=callback, metavar="PATH", is_eager=True, **kwargs, diff --git a/src/ape/pytest/plugin.py b/src/ape/pytest/plugin.py index f364ec9f13..123301a06e 100644 --- a/src/ape/pytest/plugin.py +++ b/src/ape/pytest/plugin.py @@ -47,6 +47,7 @@ def add_option(*names, **kwargs): help="A comma-separated list of contract:method-name glob-patterns to ignore.", ) add_option("--coverage", action="store_true", help="Collect contract coverage.") + add_option("--project", action="store", help="Change Ape's project") # NOTE: Other pytest plugins, such as hypothesis, should integrate with pytest separately @@ -79,6 +80,9 @@ def is_module(v): from ape.pytest.runners import PytestApeRunner from ape.utils.basemodel import ManagerAccessMixin + if project := config.getoption("--project"): + ManagerAccessMixin.local_project.chdir(project) + # Register the custom Ape test runner config_wrapper = ConfigWrapper(config) receipt_capture = ReceiptCapture(config_wrapper) diff --git a/src/ape_test/_cli.py b/src/ape_test/_cli.py index fedb4b3b7e..a98baaffd5 100644 --- a/src/ape_test/_cli.py +++ b/src/ape_test/_cli.py @@ -7,7 +7,7 @@ import pytest from click import Command -from ape.cli.options import ape_cli_context, project_option +from ape.cli.options import ape_cli_context from ape.logging import LogLevel, _get_level @@ -79,7 +79,6 @@ def parse_args(self, ctx, args: list[str]) -> list[str]: @ape_cli_context( default_log_level=LogLevel.WARNING.value, ) -@project_option(type=Path, cd=True) @click.option( "-w", "--watch", @@ -104,7 +103,7 @@ def parse_args(self, ctx, args: list[str]) -> list[str]: help="Delay between polling cycles for `ape test --watch`. Defaults to 0.5 seconds.", ) @click.argument("pytest_args", nargs=-1, type=click.UNPROCESSED) -def cli(cli_ctx, project, watch, watch_folders, watch_delay, pytest_args): +def cli(cli_ctx, watch, watch_folders, watch_delay, pytest_args): pytest_arg_ls = [*pytest_args] if pytest_verbosity := cli_ctx.get("pytest_verbosity"): pytest_arg_ls.append(pytest_verbosity)