Source code for solitude.common.contract_objectlist

# Copyright (c) 2019, Solitude Developers
#
# This source code is licensed under the BSD-3-Clause license found in the
# COPYING file in the root directory of this source tree

import os
import hashlib
import json
from typing import List, Tuple, Dict, Optional  # noqa
from solitude._internal import type_assert, value_assert, RaiseForParam


def make_unique_built_contract_filename(unitname: str, contractname: str):
    """Create a unique filename to store the contract identified by (unitname, contractname)

    This function shrinks the 'unitname', which may contain a path, keeping the
    uniqueness of the (unitname, contractname) tuple.

    :param unitname: source unit containing the contract
    :param contractname: name of the contract
    :return: unique filename
    """
    h = hashlib.md5()
    h.update(unitname.encode("utf-8"))
    h.update(b"\0")
    h.update(contractname.encode("utf-8"))
    return "build_" + contractname + "_" + h.hexdigest() + ".json"


def file_is_built_contract(filename: str):
    """Verify from the filename whether a file may contain compiled contract
    information.

    :param filename: name of the file (excluding directory)
    :return: True if the filename is valid for a solitude compiled contract
        output filename.
    """
    return filename.startswith("build_") and filename.endswith(".json")


[docs]class ContractObjectList: """A collection of compiled contracts """
[docs] def __init__(self): """Create an empty collection of compiled contracts """ self._contracts = {} # type: Dict[Tuple[str, str], dict] self._name_to_units = {}
[docs] def add_contract(self, unitname: str, contractname: str, contract: dict): """Add a contract, uniquely identified by (unitname, contractname). :param unitname: source unit containing the contract :param contractname: name of the contract :param contract: contract data dictionary, as produced by the compiler module """ key = (unitname, contractname) if key in self._contracts: raise CompilerError([FileMessage( type="duplicate", unitname=unitname + ":" + contractname, line=None, column=None, message="Duplicate contract identifier found")]) self._contracts[key] = contract try: self._name_to_units[contractname].append(unitname) except KeyError: self._name_to_units[contractname] = [unitname]
[docs] def add_directory(self, path: str) -> None: """Add all contracts from a directory. :param path: path of the directory containing the contracts data. """ for filename in os.listdir(path): if file_is_built_contract(filename): with open(os.path.join(path, filename)) as fp: contract = json.load(fp) self.add_contract( contract["_solitude"]["unitName"], contract["_solitude"]["contractName"], contract)
[docs] def save_directory(self, path: str) -> None: """Save all contracts to a directory. :param path: path of destination directory; the directory must exist. """ os.makedirs(path, exist_ok=True) for (unitname, contractname), contract in self._contracts.items(): filename = make_unique_built_contract_filename(unitname, contractname) with open(os.path.join(path, filename), "w") as fp: json.dump(contract, fp, indent=2)
[docs] def update(self, other: "ContractObjectList") -> None: """Add all contracts from other ContractObjectList. :param other: other ContractObjectList with contracts to add """ with RaiseForParam("other"): type_assert(other, ContractObjectList) for (unitname, contractname), contract in other._contracts.items(): self.add_contract(unitname, contractname, contract)
[docs] def find(self, suffix: Optional[str], contractname: str) -> List[Tuple[str, str]]: """Find contracts by unitname suffix and full contractname. Example: ("erc20/ERC20.sol", "ERC20") matches ("/home/user/contracts/erc20/ERC20.sol", "ERC20"). :param suffix: suffix to match the contract source unit name, or None; if it is None, any unit name is matched. :param contractname: full name of the contract """ with RaiseForParam("suffix"): type_assert(suffix, (str, type(None))) with RaiseForParam("contractname"): type_assert(contractname, str) out = [] units = self._name_to_units.get(contractname, []) for unit in units: if suffix is None or unit.endswith(suffix): out.append((unit, contractname)) return out
[docs] def select(self, selector: str) -> dict: """Find a single contract matching the contract selector string. The selector string is a string in either of the following forms: - "{suffix}:{contractname}": source unit name suffix and contract name, separated by ':'. Example: "erc20/ERC20.sol:ERC20" matches contract named "ERC20" in source unit "/home/user/contracts/erc20/ERC20.sol". - "{contractname}": only the contract name. Example: "ERC20" matches contract named "ERC20". If the selector matches multiple contract, this function will raise an exception of type ValueError. :param selector: contract selector """ with RaiseForParam("selector"): type_assert(selector, str) if selector.count(":") == 1: suffix, contractname = selector.split(":") else: suffix, contractname = None, selector value_assert( "/" not in contractname, "Invalid contract selector syntax. Either 'path/suffix:ContractName' or 'ContractName'") contracts = self.find(suffix, contractname) value_assert( len(contracts) != 0, "Contract not found") value_assert( len(contracts) == 1, "Contract selector matched multiple contracts") return self._contracts[contracts[0]]
@property def contracts(self) -> Dict[Tuple[str, str], dict]: """All contracts, as a dictionary of (unitname, contractname) -> data """ return self._contracts.copy()