Skip to content
/ ape Public
forked from ApeWorX/ape

Commit 898fc8d

Browse files
authored
feat: support Github dependencies (#284)
1 parent e9d5935 commit 898fc8d

File tree

8 files changed

+178
-43
lines changed

8 files changed

+178
-43
lines changed

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,10 @@
8383
"requests>=2.25.1,<3.0",
8484
"importlib-metadata",
8585
"singledispatchmethod ; python_version<'3.8'",
86-
"tqdm>=4.62.3,<5.0",
8786
"IPython>=7.25",
8887
"pytest>=6.0,<7.0",
8988
"rich>=10.14,<11",
89+
"tqdm>=4.62.3,<5.0",
9090
"typing-extensions ; python_version<'3.8'",
9191
"web3[tester]>=5.25.0,<6.0.0",
9292
],

src/ape/__init__.py

+2-11
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,7 @@
1414
from .managers.networks import NetworkManager as _NetworkManager
1515
from .managers.project import ProjectManager as _ProjectManager
1616
from .plugins import PluginManager as _PluginManager
17-
from .utils import get_package_version
18-
19-
__version__ = get_package_version(__name__)
20-
21-
22-
# NOTE: DO NOT OVERWRITE
23-
_python_version = (
24-
f"{_sys.version_info.major}.{_sys.version_info.minor}"
25-
f".{_sys.version_info.micro} {_sys.version_info.releaselevel}"
26-
)
17+
from .utils import USER_AGENT
2718

2819
# Wiring together the application
2920

@@ -35,7 +26,7 @@
3526
DATA_FOLDER=_Path.home().joinpath(".ape"),
3627
# NOTE: For all HTTP requests we make
3728
REQUEST_HEADER={
38-
"User-Agent": f"Ape/{__version__} (Python/{_python_version})",
29+
"User-Agent": USER_AGENT,
3930
},
4031
# What we are considering to be the starting project directory
4132
PROJECT_FOLDER=_Path.cwd(),

src/ape/api/compiler.py

+3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
from pathlib import Path
22
from typing import List, Set
33

4+
from ape.api import ConfigItem
45
from ape.types import ContractType
56

67
from .base import abstractdataclass, abstractmethod
78

89

910
@abstractdataclass
1011
class CompilerAPI:
12+
config: ConfigItem
13+
1114
@property
1215
@abstractmethod
1316
def name(self) -> str:

src/ape/cli/paramtype.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import click
55
from click import Context, Parameter
66

7+
from ape.utils import get_all_files_in_directory
8+
79

810
class Path(click.Path):
911
"""
@@ -25,7 +27,4 @@ def convert(
2527
self, value: Any, param: Optional["Parameter"], ctx: Optional["Context"]
2628
) -> List[PathLibPath]:
2729
path = super().convert(value, param, ctx)
28-
if path.is_dir():
29-
return list(path.rglob("*.*"))
30-
31-
return [path]
30+
return get_all_files_in_directory(path)

src/ape/managers/compilers.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ def registered_compilers(self) -> Dict[str, CompilerAPI]:
2323
registered_compilers = {}
2424

2525
for plugin_name, (extensions, compiler_class) in self.plugin_manager.register_compiler:
26-
# TODO: Add config via ``self.config.get_config(plugin_name)``
27-
compiler = compiler_class()
26+
config = self.config.get_config(plugin_name)
27+
compiler = compiler_class(config=config)
2828

2929
for extension in extensions:
3030

@@ -42,7 +42,7 @@ def compile(self, contract_filepaths: List[Path]) -> Dict[str, ContractType]:
4242
for extension in extensions:
4343
paths_to_compile = [path for path in contract_filepaths if path.suffix == extension]
4444
for path in paths_to_compile:
45-
logger.info(f"Compiling '{self._get_contract_path(path)}'")
45+
logger.info(f"Compiling '{self._get_contract_path(path)}'.")
4646

4747
for contract_type in self.registered_compilers[extension].compile(paths_to_compile):
4848

src/ape/managers/config.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def __post_init__(self):
3434
# Top level config items
3535
self.name = user_config.pop("name", "")
3636
self.version = user_config.pop("version", "")
37-
self.dependencies = user_config.pop("dependencies", [])
37+
self.dependencies = user_config.pop("dependencies", {})
3838

3939
for plugin_name, config_class in self.plugin_manager.config_class:
4040
# NOTE: `dict.pop()` is used for checking if all config was processed

src/ape/managers/project.py

+68-22
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import json
22
from pathlib import Path
3-
from typing import Dict, List, Optional
3+
from typing import Collection, Dict, List, Optional, Union
44

55
import requests
66
from dataclassy import dataclass
@@ -9,12 +9,24 @@
99
from ape.exceptions import ProjectError
1010
from ape.managers.networks import NetworkManager
1111
from ape.types import Checksum, Compiler, ContractType, PackageManifest, Source
12-
from ape.utils import compute_checksum
12+
from ape.utils import compute_checksum, get_all_files_in_directory, github_client
1313

1414
from .compilers import CompilerManager
1515
from .config import ConfigManager
1616

1717

18+
def _create_source_dict(contracts_paths: Collection[Path]) -> Dict[str, Source]:
19+
return {
20+
str(source): Source( # type: ignore
21+
checksum=Checksum( # type: ignore
22+
algorithm="md5", hash=compute_checksum(source.read_bytes())
23+
),
24+
urls=[],
25+
)
26+
for source in contracts_paths
27+
}
28+
29+
1830
@dataclass
1931
class ProjectManager:
2032
path: Path
@@ -29,17 +41,57 @@ def __post_init__(self):
2941
self.path = Path(self.path)
3042

3143
self.dependencies = {
32-
manifest.name: manifest
33-
for manifest in map(self._extract_manifest, self.config.dependencies)
44+
n: self._extract_manifest(n, dep_id) for n, dep_id in self.config.dependencies.items()
3445
}
3546

36-
def _extract_manifest(self, manifest_uri: str) -> PackageManifest:
37-
manifest_dict = requests.get(manifest_uri).json()
38-
# TODO: Handle non-manifest URLs e.g. Ape/Brownie projects, Hardhat/Truffle projects, etc.
39-
if "name" not in manifest_dict:
40-
raise ProjectError("Dependencies must have a name.")
41-
42-
return PackageManifest.from_dict(manifest_dict)
47+
def _extract_manifest(self, name: str, download_path: str) -> PackageManifest:
48+
packages_path = self.config.DATA_FOLDER / "packages"
49+
packages_path.mkdir(exist_ok=True, parents=True)
50+
target_path = packages_path / name
51+
target_path.mkdir(exist_ok=True, parents=True)
52+
53+
if download_path.startswith("https://") or download_path.startswith("http://"):
54+
manifest_file_path = target_path / "manifest.json"
55+
if manifest_file_path.exists():
56+
manifest_dict = json.loads(manifest_file_path.read_text())
57+
else:
58+
# Download manifest
59+
response = requests.get(download_path)
60+
manifest_file_path.write_text(response.text)
61+
manifest_dict = response.json()
62+
63+
if "name" not in manifest_dict:
64+
raise ProjectError("Dependencies must have a name.")
65+
66+
return PackageManifest.from_dict(manifest_dict)
67+
else:
68+
# Github dependency (format: <org>/<repo>@<version>)
69+
try:
70+
path, version = download_path.split("@")
71+
except ValueError:
72+
raise ValueError("Invalid Github ID. Must be given as <org>/<repo>@<version>")
73+
74+
package_contracts_path = target_path / "contracts"
75+
is_cached = len([p for p in target_path.iterdir()]) > 0
76+
77+
if not is_cached:
78+
github_client.download_package(path, version, target_path)
79+
80+
if not package_contracts_path.exists():
81+
raise ProjectError(
82+
"Dependency does not have a support structure. Expecting 'contracts/' path."
83+
)
84+
85+
manifest = PackageManifest()
86+
sources = [
87+
s
88+
for s in get_all_files_in_directory(package_contracts_path)
89+
if s.name not in ("package.json", "package-lock.json")
90+
and s.suffix in self.compilers.registered_compilers
91+
]
92+
manifest.sources = _create_source_dict(sources)
93+
manifest.contractTypes = self.compilers.compile(sources)
94+
return manifest
4395

4496
def __str__(self) -> str:
4597
return f'Project("{self.path}")'
@@ -146,8 +198,11 @@ def find_in_dir(dir_path: Path) -> Optional[Path]:
146198
return find_in_dir(self.contracts_folder)
147199

148200
def load_contracts(
149-
self, file_paths: Optional[List[Path]] = None, use_cache: bool = True
201+
self, file_paths: Optional[Union[List[Path], Path]] = None, use_cache: bool = True
150202
) -> Dict[str, ContractType]:
203+
if isinstance(file_paths, Path):
204+
file_paths = [file_paths]
205+
151206
# Load a cached or clean manifest (to use for caching)
152207
manifest = use_cache and self.cached_manifest or PackageManifest()
153208
cached_sources = manifest.sources or {}
@@ -190,16 +245,7 @@ def file_needs_compiling(source: Path) -> bool:
190245

191246
# Update cached contract types & source code entries in cached manifest
192247
manifest.contractTypes = contract_types
193-
cached_sources = {
194-
str(source): Source( # type: ignore
195-
checksum=Checksum( # type: ignore
196-
algorithm="md5", hash=compute_checksum(source.read_bytes())
197-
),
198-
urls=[],
199-
)
200-
for source in sources
201-
}
202-
manifest.sources = cached_sources
248+
manifest.sources = _create_source_dict(sources)
203249

204250
# NOTE: Cache the updated manifest to disk (so ``self.cached_manifest`` reads next time)
205251
self.manifest_cachefile.write_text(json.dumps(manifest.to_dict()))

0 commit comments

Comments
 (0)