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

Brownie Package Manager #390

Merged
merged 19 commits into from
Apr 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
22f1639
modify cwd prior to compiling a project
iamdefinitelyahuman Apr 4, 2020
68b01ad
feat: brownie pm cli tool
iamdefinitelyahuman Apr 4, 2020
1668f7b
update erc1319 to ethpm
iamdefinitelyahuman Apr 5, 2020
3e58230
allow ethpm package installation via brownie pm
iamdefinitelyahuman Apr 5, 2020
21fc00f
remove ethpm from main cli menu (temporary deprecation)
iamdefinitelyahuman Apr 5, 2020
2e0ab37
doc: add package manager documentation
iamdefinitelyahuman Apr 5, 2020
8525605
feat: add pm fixture
iamdefinitelyahuman Apr 5, 2020
1e4068b
feat: include path remappings to allow packages imports
iamdefinitelyahuman Apr 5, 2020
3c60350
do not include leading v in package versions
iamdefinitelyahuman Apr 5, 2020
4c8a399
create brownie data subfolders on load
iamdefinitelyahuman Apr 5, 2020
ca95dec
move package installation logic to project.main, install project depe…
iamdefinitelyahuman Apr 5, 2020
c90a858
test: add pm test cases
iamdefinitelyahuman Apr 6, 2020
91f8c8c
test: rename mixtest to pmtest
iamdefinitelyahuman Apr 6, 2020
fa68c27
feat: use github api token for package tag queries
iamdefinitelyahuman Apr 7, 2020
cadd053
doc: add api docs and docstrings for install_package
iamdefinitelyahuman Apr 7, 2020
47c65cb
feat: handle dependencies given as a string
iamdefinitelyahuman Apr 7, 2020
cd5dc95
feat: allow path remappings for solidity compiler
iamdefinitelyahuman Apr 7, 2020
64c546a
test: test case for remapping path of installed package
iamdefinitelyahuman Apr 7, 2020
4605756
doc: add documentation for path remappings
iamdefinitelyahuman Apr 7, 2020
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
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
- sudo apt-get update
- sudo apt-get install -y python3.6-dev npm solc
script: tox -e lint,doctest,py36
- name: "Standard Tests, Brownie Mix Tests - Python 3.7 on Bionic Linux"
- name: "Standard Tests, Package Tests - Python 3.7 on Bionic Linux"
language: python
python: 3.7
dist: bionic
Expand All @@ -38,7 +38,7 @@ jobs:
- sudo add-apt-repository -y ppa:deadsnakes/ppa
- sudo apt-get update
- sudo apt-get install -y python3.6-dev npm solc
script: tox -e py37,mixtest
script: tox -e py37,pmtest
- name: "Standard Tests, Plugin Tests - Python 3.8 on Bionic Linux"
language: python
python: 3.8
Expand Down
4 changes: 2 additions & 2 deletions brownie/_cli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
Commands:
init Initialize a new brownie project
bake Initialize from a brownie-mix template
ethpm Commands related to the ethPM package manager
compile Compiles the contract source files
pm Install and manage external packages
compile Compile the contract source files
console Load the console
test Run test cases in the tests/ folder
run Run a script in the scripts/ folder
Expand Down
3 changes: 1 addition & 2 deletions brownie/_cli/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@

def main():
args = docopt(__doc__)
_get_data_folder().joinpath("accounts").mkdir(exist_ok=True)
try:
fn = getattr(sys.modules[__name__], f"_{args['<command>']}")
except AttributeError:
Expand All @@ -41,7 +40,7 @@ def main():
try:
fn(*args["<arguments>"])
except TypeError:
print(f"Invalid arguments for command '{args['<command>']}'. Try brownie ethpm --help")
print(f"Invalid arguments for command '{args['<command>']}'. Try brownie accounts --help")
return


Expand Down
4 changes: 2 additions & 2 deletions brownie/_cli/ethpm.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ def _release(project_path, registry_address, sender):
)
if tx.status == 1:
notify("SUCCESS", f"{name} has been released!")
print(f"\nURI: {color('bright magenta')}erc1319://{registry_address}:1/{name}{color}")
print(f"\nURI: {color('bright magenta')}ethpm://{registry_address}:1/{name}{color}")
return
except Exception:
pass
Expand All @@ -161,7 +161,7 @@ def _all(project_path):
if not package_list:
path.unlink()
continue
print(f"{color('bright magenta')}erc1319://{path.name}{color}")
print(f"{color('bright magenta')}ethpm://{path.name}{color}")
for package_path in package_list:
u = "\u2514" if package_path == package_list[-1] else "\u251c"
versions = sorted(package_path.glob("*.json"))
Expand Down
114 changes: 114 additions & 0 deletions brownie/_cli/pm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#!/usr/bin/python3

import shutil
import sys
from pathlib import Path

from brownie import project
from brownie._config import _get_data_folder
from brownie.utils import color, notify
from brownie.utils.docopt import docopt

__doc__ = """Usage: brownie pm <command> [<arguments> ...] [options]

Commands:
list List available accounts
install <uri> [version] Install a new package
clone <id> [path] Make a copy of an installed package
delete <id> Delete an installed package

Options:
--help -h Display this message

Manager for packages installed from ethPM and Github. Installed packages can
be added as dependencies and imported into your own projects.

See https://eth-brownie.readthedocs.io/en/stable/package-manager.html for
more information on how to install and use packages.
"""


def main():
args = docopt(__doc__)
try:
fn = getattr(sys.modules[__name__], f"_{args['<command>']}")
except AttributeError:
print("Invalid command. Try brownie pm --help")
return
try:
fn(*args["<arguments>"])
except TypeError:
print(f"Invalid arguments for command '{args['<command>']}'. Try brownie pm --help")
return


def _list():
org_names = []
for path in _get_data_folder().joinpath("packages").iterdir():
if not path.is_dir():
continue
elif not list(i for i in path.iterdir() if i.is_dir() and "@" in i.name):
shutil.rmtree(path)
else:
org_names.append(path)

if not org_names:
print("No packages are currently installed.")
else:
print("The following packages are currently installed:")

for org_path in org_names:
packages = list(org_path.iterdir())
print(f"\n{color('bright magenta')}{org_path.name}{color}")
for path in packages:
u = "\u2514" if path == packages[-1] else "\u251c"
name, version = path.name.rsplit("@", maxsplit=1)
print(f" {color('bright black')}{u}\u2500{_format_pkg(org_path.name, name, version)}")


def _clone(package_id, path_str="."):
org, repo, version = _split_id(package_id)
source_path = _get_data_folder().joinpath(f"packages/{org}/{repo}@{version}")
if not source_path.exists():
raise FileNotFoundError(f"Package '{_format_pkg(org, repo, version)}' is not installed")
dest_path = Path(path_str)
if dest_path.exists():
if not dest_path.is_dir():
raise FileExistsError(f"Destination path already exists")
dest_path = dest_path.joinpath(package_id)
shutil.copytree(source_path, dest_path)
notify("SUCCESS", f"Package '{_format_pkg(org, repo, version)}' was cloned at {dest_path}")


def _delete(package_id):
org, repo, version = _split_id(package_id)
source_path = _get_data_folder().joinpath(f"packages/{org}/{repo}@{version}")
if not source_path.exists():
raise FileNotFoundError(f"Package '{_format_pkg(org, repo, version)}' is not installed")
shutil.rmtree(source_path)
notify("SUCCESS", f"Package '{_format_pkg(org, repo, version)}' has been deleted")


def _install(uri):
package_id = project.main.install_package(uri)
org, repo, version = _split_id(package_id)
notify("SUCCESS", f"Package '{_format_pkg(org, repo, version)}' has been installed")


def _split_id(package_id):
try:
path, version = package_id.split("@")
org, repo = path.split("/")
return org, repo, version
except ValueError:
raise ValueError(
"Invalid package ID. Must be given as [ORG]/[REPO]@[VERSION]"
f"\ne.g. {_format_pkg('openzeppelin', 'openzeppelin-contracts', 'v2.5.0')}"
) from None


def _format_pkg(org, repo, version):
return (
f"{color('blue')}{org}/{color('bright blue')}{repo}"
f"{color('blue')}@{color('bright blue')}{version}{color}"
)
24 changes: 21 additions & 3 deletions brownie/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
BROWNIE_FOLDER = Path(__file__).parent
DATA_FOLDER = Path.home().joinpath(".brownie")

DATA_SUBFOLDERS = ("accounts", "ethpm", "packages")

REPLACE = ["active_network", "networks"]

EVM_EQUIVALENTS = {"atlantis": "byzantium", "agharta": "petersburg"}
Expand Down Expand Up @@ -88,9 +90,6 @@ def _load_default_config() -> "ConfigDict":
# Loads the default configuration settings from brownie/data/config.yaml
base_config = BROWNIE_FOLDER.joinpath("data/brownie-config.yaml")

if not DATA_FOLDER.exists():
DATA_FOLDER.mkdir()

config = _Singleton("Config", (ConfigDict,), {})(_load_config(base_config)) # type: ignore
config["active_network"] = {"name": None}
_modify_hypothesis_settings(config)
Expand All @@ -116,6 +115,14 @@ def _load_project_compiler_config(project_path: Optional[Path]) -> Dict:
return compiler_data


def _load_project_dependencies(project_path: Path) -> Dict:
compiler_data = _load_config(project_path.joinpath("brownie-config"))
dependencies = compiler_data.get("dependencies", [])
if isinstance(dependencies, str):
dependencies = [dependencies]
return dependencies


def _modify_network_config(network: str = None) -> Dict:
"""Modifies the 'active_network' configuration settings"""
CONFIG._unlock()
Expand Down Expand Up @@ -166,6 +173,17 @@ def _get_data_folder() -> Path:
return DATA_FOLDER


def _make_data_folders(data_folder: Path) -> None:
# create data folder structure
data_folder.mkdir(exist_ok=True)
for folder in DATA_SUBFOLDERS:
data_folder.joinpath(folder).mkdir(exist_ok=True)


# create data folders
_make_data_folders(DATA_FOLDER)


# create argv object
ARGV = _Singleton("Argv", (defaultdict,), {})(lambda: None) # type: ignore

Expand Down
4 changes: 4 additions & 0 deletions brownie/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,7 @@ class InvalidManifest(Exception):

class UnsupportedLanguage(Exception):
pass


class InvalidPackage(Exception):
pass
47 changes: 45 additions & 2 deletions brownie/project/compiler/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from eth_utils import remove_0x_prefix
from semantic_version import Version

from brownie._config import _get_data_folder
from brownie.exceptions import UnsupportedLanguage
from brownie.project import sources
from brownie.project.compiler.solidity import ( # NOQA: F401
Expand Down Expand Up @@ -48,17 +49,21 @@ def compile_and_format(
silent: bool = True,
allow_paths: Optional[str] = None,
interface_sources: Optional[Dict[str, str]] = None,
remappings: Optional[list] = None,
) -> Dict:
"""Compiles contracts and returns build data.

Args:
contracts: a dictionary in the form of {'path': "source code"}
contract_sources: a dictionary in the form of {'path': "source code"}
solc_version: solc version to compile with (use None to set via pragmas)
optimize: enable solc optimizer
runs: optimizer runs
evm_version: evm version to compile for
silent: verbose reporting
allow_paths: compiler allowed filesystem import path
interface_sources: dictionary of interfaces as {'path': "source code"}
remappings: list of solidity path remappings


Returns:
build data dict
Expand Down Expand Up @@ -108,8 +113,9 @@ def compile_and_format(
to_compile = {k: v for k, v in contract_sources.items() if k in path_list}

input_json = generate_input_json(
to_compile, optimize, runs, evm_version, language, interfaces
to_compile, optimize, runs, evm_version, language, interfaces, remappings
)

output_json = compile_from_input_json(input_json, silent, allow_paths)

output_json["contracts"] = {
Expand All @@ -127,6 +133,7 @@ def generate_input_json(
evm_version: Union[int, str, None] = None,
language: str = "Solidity",
interface_sources: Optional[Dict[str, str]] = None,
remappings: Optional[list] = None,
) -> Dict:

"""Formats contracts to the standard solc input json.
Expand All @@ -137,6 +144,8 @@ def generate_input_json(
runs: optimizer runs
evm_version: evm version to compile for
language: source language (Solidity or Vyper)
interface_sources: dictionary of interfaces as {'path': "source code"}
remappings: list of solidity path remappings

Returns: dict
"""
Expand All @@ -155,6 +164,7 @@ def generate_input_json(
input_json["settings"]["evmVersion"] = evm_version
if language == "Solidity":
input_json["settings"]["optimizer"] = {"enabled": optimize, "runs": runs if optimize else 0}
input_json["settings"]["remappings"] = _get_solc_remappings(remappings)
input_json["sources"] = _sources_dict(contract_sources, language)

if interface_sources:
Expand All @@ -166,6 +176,36 @@ def generate_input_json(
return input_json


def _get_solc_remappings(remappings: Optional[list]) -> list:
if remappings is None:
remap_dict: Dict = {}
elif isinstance(remappings, str):
remap_dict = dict([remappings.split("=")])
else:
remap_dict = dict(i.split("=") for i in remappings)

for path in _get_data_folder().joinpath("packages").iterdir():
key = next((k for k, v in remap_dict.items() if v.startswith(path.name)), None)
if key:
remap_dict[key] = path.parent.joinpath(remap_dict[key]).as_posix()
else:
remap_dict[path.name] = path.as_posix()

return [f"{k}={v}" for k, v in remap_dict.items()]


def _get_allow_paths(allow_paths: Optional[str], remappings: list) -> str:
# generate the final allow_paths field based on path remappings
path_list = [] if allow_paths is None else [allow_paths]

remapping_paths = [i[i.index("=") + 1 :] for i in remappings]
data_path = _get_data_folder().joinpath("packages").as_posix()
remapping_paths = [i for i in remapping_paths if not i.startswith(data_path)]

path_list = path_list + [data_path] + remapping_paths
return ",".join(path_list)


def compile_from_input_json(
input_json: Dict, silent: bool = True, allow_paths: Optional[str] = None
) -> Dict:
Expand All @@ -183,8 +223,11 @@ def compile_from_input_json(

if input_json["language"] == "Vyper":
return vyper.compile_from_input_json(input_json, silent, allow_paths)

if input_json["language"] == "Solidity":
allow_paths = _get_allow_paths(allow_paths, input_json["settings"]["remappings"])
return solidity.compile_from_input_json(input_json, silent, allow_paths)

raise UnsupportedLanguage(f"{input_json['language']}")


Expand Down
6 changes: 4 additions & 2 deletions brownie/project/ethpm.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@

from . import compiler

URI_REGEX = r"""^(?:erc1319://|)([^/:\s]*)(?::[0-9]+|)/([a-z][a-z0-9_-]{0,255})@([^\s:/'";]*)$"""
URI_REGEX = (
r"""^(?:erc1319://|ethpm://|)([^/:\s]*)(?::[0-9]+|)/([a-z][a-z0-9_-]{0,255})@([^\s:/'";]*)$"""
)

REGISTRY_ABI = [
{
Expand Down Expand Up @@ -297,7 +299,7 @@ def install_package(project_path: Path, uri: str, replace_existing: bool = False

Args:
project_path: Path to the root folder of the project
uri: manifest URI, can be erc1319 or ipfs
uri: manifest URI, can be erc1319, ethpm or ipfs
replace_existing: if True, existing files will be overwritten when
installing the package

Expand Down
Loading