Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow configuring base_path: in projects #2295

Merged
merged 9 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/userguides/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,27 @@ plugin:

This helps keep your secrets out of Ape!

## Base Path

Change the base path if it is different than your project root.
For example, imagine a project structure like:

```
project
└── src/
└── contracts/
└── MyContract.sol
```

In this case, you want to configure Ape like:

```yaml
base_path: src
```

This way, `MyContract.vy`'s source ID will be `"contracts/Factory.vy"` and not `"src/contracts/Factory.vy"`.
Some dependencies, such as python-based ones like `snekmate`, use this structure.

## Contracts Folder

Specify a different path to your `contracts/` directory.
Expand Down
6 changes: 6 additions & 0 deletions src/ape/api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,12 @@ def __init__(self, *args, **kwargs):
The name of the project.
"""

base_path: Optional[str] = None
"""
Use this when the project's base-path is not the
root of the project.
"""

request_headers: dict = {}
"""
Extra request headers for all HTTP requests.
Expand Down
1 change: 0 additions & 1 deletion src/ape/managers/networks.py
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,6 @@ def default_ecosystem(self) -> EcosystemAPI:
only a single ecosystem installed, such as Ethereum, then get
that ecosystem.
"""

return self.ecosystems[self.default_ecosystem_name]

def set_default_ecosystem(self, ecosystem_name: str):
Expand Down
20 changes: 12 additions & 8 deletions src/ape/managers/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -2118,16 +2118,20 @@ def __init__(
config_override: Optional[dict] = None,
) -> None:
self._session_source_change_check: set[str] = set()
self.path = Path(path).resolve()
# A local project uses a special manifest.
self.manifest_path = manifest_path or self.path / ".build" / "__local__.json"
manifest = self.load_manifest()

# NOTE: Set this before super() because needed for self.config read.
self._config_override = config_override or {}

self._base_path = Path(path).resolve()

# A local project uses a special manifest.
self.manifest_path = manifest_path or self._base_path / ".build" / "__local__.json"
manifest = self.load_manifest()

super().__init__(manifest, config_override=self._config_override)

self.path = self._base_path / (self.config.base_path or "")

# NOTE: Avoid pointlessly adding info to the __local__ manifest.
# This is mainly for dependencies.
if self.manifest_path.stem != "__local__" and not manifest.sources:
Expand Down Expand Up @@ -2276,7 +2280,7 @@ def project_api(self) -> ProjectAPI:
]
plugins = [t for t in project_classes if not issubclass(t, ApeProject)]
for api in plugins:
if instance := api.attempt_validate(path=self.path):
if instance := api.attempt_validate(path=self._base_path):
return instance

# If no other APIs worked but we have a default Ape project, use that!
Expand All @@ -2286,10 +2290,10 @@ def project_api(self) -> ProjectAPI:

# For some reason we were just not able to create a project here.
# I am not sure this is even possible.
raise ProjectError(f"'{self.path.name}' is not recognized as a project.")
raise ProjectError(f"'{self._base_path.name}' is not recognized as a project.")

def _get_ape_project_api(self) -> Optional[ApeProject]:
if instance := ApeProject.attempt_validate(path=self.path):
if instance := ApeProject.attempt_validate(path=self._base_path):
return cast(ApeProject, instance)

return None
Expand All @@ -2301,7 +2305,7 @@ def name(self) -> str:
elif name := self.manifest.name:
return name

return self.path.name.replace("_", "-").lower()
return self._base_path.name.replace("_", "-").lower()

@cached_property
def config(self) -> ApeConfig:
Expand Down
50 changes: 46 additions & 4 deletions tests/functional/test_project.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
import os
import shutil
from pathlib import Path

Expand All @@ -12,7 +13,7 @@
from ape import Project
from ape.api.projects import ApeProject
from ape.contracts import ContractContainer
from ape.exceptions import ProjectError
from ape.exceptions import ConfigError, ProjectError
from ape.logging import LogLevel
from ape.utils import create_tempdir
from ape_pm import BrownieProject, FoundryProject
Expand Down Expand Up @@ -93,6 +94,31 @@ def test_path(project):
assert project.path is not None


def test_path_configured(project):
"""
Simulating package structures like snekmate.
"""
madeup_name = "snakemate"
with create_tempdir(name=madeup_name) as temp_dir:
subdir = temp_dir / "src"
contracts_folder = subdir / madeup_name
contracts_folder.mkdir(parents=True)
contract = contracts_folder / "snake.json"
abi = [{"name": "foo", "type": "fallback", "stateMutability": "nonpayable"}]
contract.write_text(json.dumps(abi), encoding="utf8")

snekmate = Project(
temp_dir, config_override={"base_path": "src", "contracts_folder": madeup_name}
)
assert snekmate.name == madeup_name
assert snekmate.path == subdir
assert snekmate.contracts_folder == contracts_folder

actual = snekmate.load_contracts()
assert "snake" in actual
assert actual["snake"].source_id == f"{madeup_name}/snake.json"


def test_name(project):
assert project.name == project.path.name

Expand Down Expand Up @@ -641,6 +667,21 @@ def test_init(self, with_dependencies_project_path):
# Manifest should have been created by default.
assert not project.manifest_path.is_file()

def test_init_invalid_config(self):
here = os.curdir
with create_tempdir() as temp_dir:
cfgfile = temp_dir / "ape-config.yaml"
# Name is invalid!
cfgfile.write_text("name:\n {asdf}")

os.chdir(temp_dir)
expected = r"[.\n]*Input should be a valid string\n-->1: name:\n 2: {asdf}[.\n]*"
try:
with pytest.raises(ConfigError, match=expected):
_ = Project(temp_dir)
finally:
os.chdir(here)

def test_config_override(self, with_dependencies_project_path):
contracts_folder = with_dependencies_project_path / "my_contracts"
config = {"contracts_folder": contracts_folder.name}
Expand Down Expand Up @@ -760,11 +801,12 @@ def gitmodules(self):
)

def test_extract_config(self, foundry_toml, gitmodules, mock_github):
with ape.Project.create_temporary_project() as temp_project:
cfg_file = temp_project.path / "foundry.toml"
with create_tempdir() as temp_dir:
cfg_file = temp_dir / "foundry.toml"
cfg_file.write_text(foundry_toml, encoding="utf8")
gitmodules_file = temp_project.path / ".gitmodules"
gitmodules_file = temp_dir / ".gitmodules"
gitmodules_file.write_text(gitmodules, encoding="utf8")
temp_project = Project(temp_dir)

api = temp_project.project_api
mock_github.get_repo.return_value = {"default_branch": "main"}
Expand Down
24 changes: 1 addition & 23 deletions tests/integration/cli/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import pytest

from ape import Project
from ape.utils import create_tempdir
from tests.conftest import ApeSubprocessRunner
from tests.integration.cli.utils import run_once

Expand Down Expand Up @@ -33,26 +34,3 @@ def test_help(ape_cli, runner):
rf"test\s*Launches pytest{anything}"
)
assert re.match(expected.strip(), result.output)


@run_once
def test_invalid_config():
# Using subprocess runner so we re-hit the init of the cmd.
runner = ApeSubprocessRunner("ape")
here = os.curdir
with Project.create_temporary_project() as tmp:
cfgfile = tmp.path / "ape-config.yaml"
# Name is invalid!
cfgfile.write_text("name:\n {asdf}")

os.chdir(tmp.path)
result = runner.invoke("--help")
os.chdir(here)

expected = """
Input should be a valid string
-->1: name:
2: {asdf}
""".strip()
assert result.exit_code != 0
assert expected in result.output
Loading