Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
eth-brownie / brownie / _config.py
Size: Mime:
#!/usr/bin/python3
import copy
import json
import os
import re
import shutil
import sys
import warnings
from collections import defaultdict
from pathlib import Path
from typing import Any, Dict, List, Optional

import yaml
from dotenv import dotenv_values, load_dotenv
from hypothesis import Phase
from hypothesis import settings as hp_settings
from hypothesis.database import DirectoryBasedExampleDatabase

from brownie._expansion import expand_posix_vars
from brownie._singleton import _Singleton

__version__ = "1.19.2"

BROWNIE_FOLDER = Path(__file__).parent
DATA_FOLDER = Path.home().joinpath(".brownie")

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

EVM_EQUIVALENTS = {"atlantis": "byzantium", "agharta": "petersburg"}

python_version = (
    f"{sys.version_info.major}.{sys.version_info.minor}"
    f".{sys.version_info.micro} {sys.version_info.releaselevel}"
)
REQUEST_HEADERS = {"User-Agent": f"Brownie/{__version__} (Python/{python_version})"}


class ConfigContainer:
    def __init__(self):
        base_config = _load_config(BROWNIE_FOLDER.joinpath("data/default-config.yaml"))
        if Path.home().joinpath("brownie-config.yaml").exists():
            home_config = _load_config(Path.home().joinpath("brownie-config.yaml"))
            _recursive_update(base_config, home_config)

        network_config = _load_config(_get_data_folder().joinpath("network-config.yaml"))

        self.networks = {}
        for value in network_config["development"]:
            key = value["id"]
            if key in self.networks:
                raise ValueError(f"Multiple networks using ID '{key}'")
            self.networks[key] = value
        for value in [x for i in network_config["live"] for x in i["networks"]]:
            key = value["id"]
            if key in self.networks:
                raise ValueError(f"Multiple networks using ID '{key}'")
            self.networks[key] = value

        # make sure chainids are always strings
        for network, values in self.networks.items():
            if "chainid" in values:
                self.networks[network]["chainid"] = str(values["chainid"])

        self.argv = defaultdict(lambda: None)
        self.settings = _Singleton("settings", (ConfigDict,), {})(base_config)
        self._active_network = None

        self.settings._lock()
        _modify_hypothesis_settings(self.settings["hypothesis"], "brownie-base", "default")

    def set_active_network(self, id_: str = None) -> Dict:
        """Modifies the 'active_network' configuration settings"""
        if id_ is None:
            id_ = self.settings["networks"]["default"]

        network = copy.deepcopy(self.networks[id_])
        key = "development" if "cmd" in network else "live"
        network["settings"] = self.settings["networks"][key].copy()

        if (
            key == "development"
            and isinstance(network["cmd_settings"], dict)
            and "fork" in network["cmd_settings"]
        ):

            fork = network["cmd_settings"]["fork"]
            if fork in self.networks:
                network["cmd_settings"]["fork"] = self.networks[fork]["host"]
                network["chainid"] = self.networks[fork]["chainid"]
                if "chain_id" not in network["cmd_settings"]:
                    network["cmd_settings"]["chain_id"] = int(self.networks[fork]["chainid"])
                if "explorer" in self.networks[fork]:
                    network["explorer"] = self.networks[fork]["explorer"]

            network["cmd_settings"]["fork"] = os.path.expandvars(network["cmd_settings"]["fork"])

        self._active_network = network
        return network

    def clear_active(self):
        self._active_network = None

    @property
    def active_network(self):
        if self._active_network is None:
            raise ConnectionError("No active network")
        return self._active_network

    @property
    def network_type(self):
        if self._active_network is None:
            return None
        if "cmd" in self._active_network:
            return "development"
        else:
            return "live"

    @property
    def mode(self):
        return self.argv["cli"]


class ConfigDict(dict):
    """Dict subclass that prevents adding new keys when locked"""

    def __init__(self, values: Dict = {}) -> None:
        self._locked = False
        super().__init__()
        self.update(values)

    def __setitem__(self, key: str, value: Any) -> None:
        if self._locked and key not in self:
            raise KeyError(f"{key} is not a known config setting")
        if type(value) is dict:
            value = ConfigDict(value)
        super().__setitem__(key, value)

    def update(self, arg):  # type: ignore
        for k, v in arg.items():
            self.__setitem__(k, v)

    def _lock(self) -> None:
        """Locks the dict so that new keys cannot be added"""
        for v in [i for i in self.values() if type(i) is ConfigDict]:
            v._lock()
        self._locked = True

    def _unlock(self) -> None:
        """Unlocks the dict so that new keys can be added"""
        for v in [i for i in self.values() if type(i) is ConfigDict]:
            v._unlock()
        self._locked = False

    def _copy(self) -> Dict:
        config_copy = {}
        for key, value in self.items():
            if isinstance(value, ConfigDict):
                value = value._copy()
            config_copy[key] = value
        return config_copy


def _get_project_config_path(project_path: Path):
    if project_path.is_dir():
        path = project_path.joinpath("brownie-config")
    else:
        path = project_path
    suffix = next((i for i in (".yml", ".yaml", ".json") if path.with_suffix(i).exists()), None)
    if suffix is not None:
        return path.with_suffix(suffix)
    return None


def _load_config(project_path: Path) -> Dict:
    """Loads configuration data from a file, returns as a dict"""
    path = _get_project_config_path(project_path)
    if path is None:
        return {}

    with path.open() as fp:
        if path.suffix in (".yaml", ".yml"):
            return yaml.safe_load(fp) or {}
        raw_json = fp.read()
    valid_json = re.sub(r'\/\/[^"]*?(?=\n|$)', "", raw_json)
    return json.loads(valid_json)


def _load_project_config(project_path: Path) -> None:
    """Loads configuration settings from a project's brownie-config.yaml"""
    config_path = project_path.joinpath("brownie-config")
    config_data = _load_config(config_path)
    config_vars = _load_project_envvars(project_path)

    if "dotenv" in config_data:
        if not isinstance(config_data["dotenv"], str):
            raise ValueError(f'Invalid value passed to dotenv: {config_data["dotenv"]}')
        env_path = project_path.joinpath(config_data["dotenv"])
        if not env_path.is_file():
            raise ValueError(f"Dotenv specified in config but not found at path: {env_path}")
        config_vars.update(dotenv_values(dotenv_path=env_path))  # type: ignore
        load_dotenv(dotenv_path=env_path)
        config_data = expand_posix_vars(config_data, config_vars)

    if not config_data:
        return

    if "network" in config_data:
        warnings.warn(
            "The `network` field in `brownie-config.yaml` has been deprecated. "
            "Network settings are now handled via `brownie networks` in the CLI. "
            f"Remove `network` from {config_path} to silence this warning.",
            DeprecationWarning,
        )
        del config_data["network"]

    # Update the network config cmd_settings with project specific cmd_settings
    if "networks" in config_data and isinstance(config_data["networks"], dict):
        for network, values in config_data["networks"].items():
            if (
                network != "default"
                and network in CONFIG.networks.keys()
                and "cmd_settings" in values
                and isinstance(values["cmd_settings"], dict)
            ):
                if "cmd_settings" in CONFIG.networks[network]:
                    _recursive_update(
                        CONFIG.networks[network]["cmd_settings"], values["cmd_settings"]
                    )
                else:
                    CONFIG.networks[network]["cmd_settings"] = values["cmd_settings"]

    CONFIG.settings._unlock()
    _recursive_update(CONFIG.settings, config_data)
    _recursive_update(CONFIG.settings, expand_posix_vars(CONFIG.settings, config_vars))

    CONFIG.settings._lock()
    if "hypothesis" in config_data:
        _modify_hypothesis_settings(config_data["hypothesis"], "brownie", "brownie-base")


def _load_project_compiler_config(project_path: Optional[Path]) -> Dict:
    if not project_path:
        return CONFIG.settings["compiler"]

    compiler_data = CONFIG.settings["compiler"]._copy()
    project_data = _load_config(project_path.joinpath("brownie-config")).get("compiler", {})
    _recursive_update(compiler_data, project_data)

    return compiler_data


def _load_project_envvars(project_path: Path) -> Dict:
    config_vars = dict(os.environ)
    if CONFIG.settings.get("dotenv"):
        dotenv_path = CONFIG.settings["dotenv"]
        if not isinstance(dotenv_path, str):
            raise ValueError(f"Invalid value passed to dotenv: {dotenv_path}")
        env_path = project_path.joinpath(dotenv_path)
        if not env_path.is_file():
            raise ValueError(f"Dotenv specified in config but not found at path: {env_path}")
        config_vars.update(dotenv_values(dotenv_path=env_path))  # type: ignore
    return config_vars


def _load_project_structure_config(project_path):
    structure = CONFIG.settings["project_structure"]._copy()

    path = _get_project_config_path(project_path)
    if path is None:
        return structure

    data = _load_config(project_path).get("project_structure", {})
    structure.update(data)
    return structure


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


def _modify_hypothesis_settings(settings, name, parent=None):
    settings = settings.copy()
    if parent is None:
        parent = hp_settings._current_profile

    if "phases" in settings:
        try:
            settings["phases"] = [getattr(Phase, k) for k, v in settings["phases"].items() if v]
        except AttributeError as exc:
            raise ValueError(f"'{exc.args[0]}' is not a valid hypothesis phase setting")

    hp_settings.register_profile(
        name,
        parent=hp_settings.get_profile(parent),
        database=DirectoryBasedExampleDatabase(_get_data_folder().joinpath("hypothesis")),
        **settings,
    )
    hp_settings.load_profile(name)


def _recursive_update(original: Dict, new: Dict) -> None:
    """Recursively merges a new dict into the original dict"""
    if not original:
        original = {}
    for k in new:
        if k in original and isinstance(new[k], dict):
            _recursive_update(original[k], new[k])
        else:
            original[k] = new[k]


def _update_argv_from_docopt(args: Dict) -> None:
    CONFIG.argv.update(dict((k.lstrip("-"), v) for k, v in args.items()))


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)

    if not data_folder.joinpath("network-config.yaml").exists():
        shutil.copyfile(
            BROWNIE_FOLDER.joinpath("data/network-config.yaml"),
            data_folder.joinpath("network-config.yaml"),
        )

    if not data_folder.joinpath("providers-config.yaml").exists():
        shutil.copyfile(
            BROWNIE_FOLDER.joinpath("data/providers-config.yaml"),
            data_folder.joinpath("providers-config.yaml"),
        )


warnings.filterwarnings("once", category=DeprecationWarning, module="brownie")

# create data folders
_make_data_folders(DATA_FOLDER)

CONFIG = _Singleton("Config", (ConfigContainer,), {})()