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..0a665d12e1 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,24 @@ def clean(self): self.sources._path_cache = None self._clear_cached_config() + def chdir(self, path: Path) -> "LocalProject": + """ + Change the local project to the new path. + + Args: + path (Path): The path of the new project. + + Returns: + LocalProject + """ + if self.path == path: + return # Already there! + + os.chdir(path) + new_local_project = LocalProject(path) + ManagerAccessMixin.local_project._cache = new_local_project + return new_local_project + 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/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