Exemplo n.º 1
0
class Project(_ProjectBase):
    """
    Top level dict-like container that holds data and objects related to
    a brownie project.

    Attributes:
        _path: Path object, absolute path to the project
        _name: Name that the project is loaded as
        _sources: project Source object
        _build: project Build object
    """
    def __init__(self, name: str, project_path: Path) -> None:
        self._path: Path = project_path
        self._name = name
        self._active = False
        self.load()

    def load(self) -> None:
        """Compiles the project contracts, creates ContractContainer objects and
        populates the namespace."""
        if self._active:
            raise ProjectAlreadyLoaded("Project is already active")

        contract_sources = _load_sources(self._path, "contracts", False)
        interface_sources = _load_sources(self._path, "interfaces", True)
        self._sources = Sources(contract_sources, interface_sources)
        self._build = Build(self._sources)

        contract_list = self._sources.get_contract_list()
        for path in list(self._path.glob("build/contracts/*.json")):
            try:
                with path.open() as fp:
                    build_json = json.load(fp)
            except json.JSONDecodeError:
                build_json = {}
            if not set(BUILD_KEYS).issubset(
                    build_json) or path.stem not in contract_list:
                path.unlink()
                continue
            if isinstance(build_json["allSourcePaths"], list):
                # this handles the format change in v1.7.0, it can be removed in a future release
                path.unlink()
                test_path = self._path.joinpath("build/tests.json")
                if test_path.exists():
                    test_path.unlink()
                continue
            if not self._path.joinpath(build_json["sourcePath"]).exists():
                path.unlink()
                continue
            self._build._add(build_json)

        interface_hashes = {}
        interface_list = self._sources.get_interface_list()
        for path in list(self._path.glob("build/interfaces/*.json")):
            try:
                with path.open() as fp:
                    build_json = json.load(fp)
            except json.JSONDecodeError:
                build_json = {}
            if not set(INTERFACE_KEYS).issubset(
                    build_json) or path.stem not in interface_list:
                path.unlink()
                continue
            self._build._add(build_json)
            interface_hashes[path.stem] = build_json["sha1"]

        self._compiler_config = _load_project_compiler_config(self._path)

        # compile updated sources, update build
        changed = self._get_changed_contracts(interface_hashes)
        self._compile(changed, self._compiler_config, False)
        self._compile_interfaces(interface_hashes)
        self._create_containers()
        self._load_deployments()

        # add project to namespaces, apply import blackmagic
        name = self._name
        self.__all__ = list(self._containers) + ["interface"]
        sys.modules[f"brownie.project.{name}"] = self  # type: ignore
        sys.modules["brownie.project"].__dict__[name] = self
        sys.modules["brownie.project"].__all__.append(name)  # type: ignore
        sys.modules["brownie.project"].__console_dir__.append(
            name)  # type: ignore
        self._namespaces = [
            sys.modules["__main__"].__dict__,
            sys.modules["brownie.project"].__dict__,
        ]
        self._active = True
        _loaded_projects.append(self)

    def _get_changed_contracts(self, compiled_hashes: Dict) -> Dict:
        # get list of changed interfaces and contracts
        new_hashes = self._sources.get_interface_hashes()
        interfaces = [
            k for k, v in new_hashes.items()
            if compiled_hashes.get(k, None) != v
        ]
        contracts = [
            i for i in self._sources.get_contract_list()
            if self._compare_build_json(i)
        ]

        # get dependents of changed sources
        final = set(contracts + interfaces)
        for contract_name in list(final):
            final.update(self._build.get_dependents(contract_name))

        # remove outdated build artifacts
        for name in [i for i in final if self._build.contains(i)]:
            self._build._remove(name)

        # get final list of changed source paths
        final.difference_update(interfaces)
        changed_set: Set = set(self._sources.get_source_path(i) for i in final)
        return {i: self._sources.get(i) for i in changed_set}

    def _compare_build_json(self, contract_name: str) -> bool:
        config = self._compiler_config
        # confirm that this contract was previously compiled
        try:
            source = self._sources.get(contract_name)
            build_json = self._build.get(contract_name)
        except KeyError:
            return True
        # compare source hashes
        if build_json["sha1"] != sha1(source.encode()).hexdigest():
            return True
        # compare compiler settings
        if _compare_settings(config, build_json["compiler"]):
            return True
        if build_json["language"] == "Solidity":
            # compare solc-specific compiler settings
            solc_config = config["solc"].copy()
            solc_config["remappings"] = None
            if _compare_settings(solc_config, build_json["compiler"]):
                return True
            # compare solc pragma against compiled version
            if Version(build_json["compiler"]
                       ["version"]) not in get_pragma_spec(source):
                return True
        return False

    def _compile_interfaces(self, compiled_hashes: Dict) -> None:
        new_hashes = self._sources.get_interface_hashes()
        changed_paths = [
            self._sources.get_source_path(k) for k, v in new_hashes.items()
            if compiled_hashes.get(k, None) != v
        ]
        if not changed_paths:
            return

        print("Generating interface ABIs...")
        changed_sources = {i: self._sources.get(i) for i in changed_paths}
        abi_json = compiler.get_abi(
            changed_sources,
            allow_paths=self._path.as_posix(),
            remappings=self._compiler_config["solc"].get("remappings", []),
        )

        for name, abi in abi_json.items():
            with self._path.joinpath(f"build/interfaces/{name}.json").open(
                    "w") as fp:
                json.dump(abi, fp, sort_keys=True, indent=2, default=sorted)
            self._build._add(abi)

    def _load_deployments(self) -> None:
        if CONFIG.network_type != "live":
            return
        chainid = CONFIG.active_network["chainid"]
        path = self._path.joinpath(f"build/deployments/{chainid}")
        path.mkdir(exist_ok=True)
        deployments = list(path.glob("*.json"))
        deployments.sort(key=lambda k: k.stat().st_mtime)
        for build_json in deployments:
            with build_json.open() as fp:
                build = json.load(fp)
            if build["contractName"] not in self._containers:
                build_json.unlink()
                continue
            if "pcMap" in build:
                contract = ProjectContract(self, build, build_json.stem)
            else:
                contract = Contract(  # type: ignore
                    build["contractName"], build_json.stem, build["abi"])
                contract._project = self
            container = self._containers[build["contractName"]]
            _add_contract(contract)
            container._contracts.append(contract)

    def _update_and_register(self, dict_: Any) -> None:
        dict_.update(self)
        if "interface" not in dict_:
            dict_["interface"] = self.interface
        self._namespaces.append(dict_)

    def _add_to_main_namespace(self) -> None:
        # temporarily adds project objects to the main namespace
        brownie: Any = sys.modules["brownie"]
        if "interface" not in brownie.__dict__:
            brownie.__dict__["interface"] = self.interface
        brownie.__dict__.update(self._containers)
        brownie.__all__.extend(self.__all__)

    def _remove_from_main_namespace(self) -> None:
        # removes project objects from the main namespace
        brownie: Any = sys.modules["brownie"]
        if brownie.__dict__.get("interface") == self.interface:
            del brownie.__dict__["interface"]
        for key in self._containers:
            brownie.__dict__.pop(key, None)
        for key in self.__all__:
            if key in brownie.__all__:
                brownie.__all__.remove(key)

    def __repr__(self) -> str:
        return f"<Project '{self._name}'>"

    def load_config(self) -> None:
        """Loads the project config file settings"""
        if isinstance(self._path, Path):
            _load_project_config(self._path)

    def close(self, raises: bool = True) -> None:
        """Removes pointers to the project's ContractContainer objects and this object."""
        if not self._active:
            if not raises:
                return
            raise ProjectNotFound("Project is not currently loaded.")

        # remove objects from namespace
        for dict_ in self._namespaces:
            for key in [
                    k for k, v in dict_.items() if v == self or
                (k in self and v == self[k])  # type: ignore
            ]:
                del dict_[key]

        # remove contracts
        for contract in [
                x for v in self._containers.values() for x in v._contracts
        ]:
            _remove_contract(contract)
        for container in self._containers.values():
            container._contracts.clear()
        self._containers.clear()

        # undo black-magic
        self._remove_from_main_namespace()
        name = self._name
        del sys.modules[f"brownie.project.{name}"]
        sys.modules["brownie.project"].__all__.remove(name)  # type: ignore
        sys.modules["brownie.project"].__console_dir__.remove(
            name)  # type: ignore
        self._active = False
        _loaded_projects.remove(self)

        # clear paths
        try:
            sys.path.remove(str(self._path))
        except ValueError:
            pass
Exemplo n.º 2
0
 def __init__(self, project_path, name):
     self._project_path = project_path
     self._name = name
     self._sources = Sources(project_path)
     self._build = Build(project_path, self._sources)
Exemplo n.º 3
0
    def load(self) -> None:
        """Compiles the project contracts, creates ContractContainer objects and
        populates the namespace."""
        if self._active:
            raise ProjectAlreadyLoaded("Project is already active")

        contract_sources = _load_sources(self._path,
                                         self._structure["contracts"], False)
        interface_sources = _load_sources(self._path,
                                          self._structure["interfaces"], True)
        self._sources = Sources(contract_sources, interface_sources)
        self._build = Build(self._sources)

        contract_list = self._sources.get_contract_list()
        for path in list(self._build_path.glob("contracts/*.json")):
            try:
                with path.open() as fp:
                    build_json = json.load(fp)
            except json.JSONDecodeError:
                build_json = {}
            if not set(BUILD_KEYS).issubset(
                    build_json) or path.stem not in contract_list:
                path.unlink()
                continue
            if isinstance(build_json["allSourcePaths"], list):
                # this handles the format change in v1.7.0, it can be removed in a future release
                path.unlink()
                test_path = self._build_path.joinpath("tests.json")
                if test_path.exists():
                    test_path.unlink()
                continue
            if not self._path.joinpath(build_json["sourcePath"]).exists():
                path.unlink()
                continue
            self._build._add(build_json)

        interface_hashes = {}
        interface_list = self._sources.get_interface_list()
        for path in list(self._build_path.glob("interfaces/*.json")):
            try:
                with path.open() as fp:
                    build_json = json.load(fp)
            except json.JSONDecodeError:
                build_json = {}
            if not set(INTERFACE_KEYS).issubset(
                    build_json) or path.stem not in interface_list:
                path.unlink()
                continue
            self._build._add(build_json)
            interface_hashes[path.stem] = build_json["sha1"]

        self._compiler_config = _load_project_compiler_config(self._path)

        # compile updated sources, update build
        changed = self._get_changed_contracts(interface_hashes)
        self._compile(changed, self._compiler_config, False)
        self._compile_interfaces(interface_hashes)
        self._create_containers()
        self._load_deployments()

        # add project to namespaces, apply import blackmagic
        name = self._name
        self.__all__ = list(self._containers) + ["interface"]
        sys.modules[f"brownie.project.{name}"] = self  # type: ignore
        sys.modules["brownie.project"].__dict__[name] = self
        sys.modules["brownie.project"].__all__.append(name)  # type: ignore
        sys.modules["brownie.project"].__console_dir__.append(
            name)  # type: ignore
        self._namespaces = [
            sys.modules["__main__"].__dict__,
            sys.modules["brownie.project"].__dict__,
        ]

        # register project for revert and reset
        _revert_register(self)

        self._active = True
        _loaded_projects.append(self)
Exemplo n.º 4
0
 def __init__(self, project_path: Optional["Path"], name: str) -> None:
     self._project_path = project_path
     self._name = name
     self._sources = Sources(project_path)
     self._build = Build(project_path, self._sources)