From c5c885b9191373b2f7da2e9139ca5bc0f2929e92 Mon Sep 17 00:00:00 2001 From: Andrei Bancioiu Date: Fri, 4 Feb 2022 10:32:41 +0200 Subject: [PATCH 01/44] On local testnet, for proxy, use it's own "economics.toml". --- erdpy/testnet/setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erdpy/testnet/setup.py b/erdpy/testnet/setup.py index 98d8bac7..bd22ad1f 100644 --- a/erdpy/testnet/setup.py +++ b/erdpy/testnet/setup.py @@ -180,7 +180,6 @@ def overwrite_genesis_file(testnet_config: TestnetConfiguration, nodes_config_fo def copy_config_to_proxy(testnet_config: TestnetConfiguration): proxy_config_source = testnet_config.proxy_config_source() - node_config_source = testnet_config.node_config_source() proxy_config = testnet_config.proxy_config_folder() makefolder(proxy_config) @@ -197,7 +196,7 @@ def copy_config_to_proxy(testnet_config: TestnetConfiguration): proxy_config) shutil.copy( - node_config_source / 'economics.toml', + proxy_config_source / 'economics.toml', proxy_config) From edaba4a8ac7dd9595a26e5d5bff1920e696c9d97 Mon Sep 17 00:00:00 2001 From: Andrei Bancioiu Date: Fri, 4 Feb 2022 10:40:10 +0200 Subject: [PATCH 02/44] Remove deprecated functionality. --- erdpy/cli_cost.py | 56 -------------------------- erdpy/proxy/__init__.py | 4 +- erdpy/proxy/cost.py | 87 ----------------------------------------- erdpy/proxy/tx_types.py | 4 -- 4 files changed, 1 insertion(+), 150 deletions(-) delete mode 100644 erdpy/cli_cost.py delete mode 100644 erdpy/proxy/cost.py delete mode 100644 erdpy/proxy/tx_types.py diff --git a/erdpy/cli_cost.py b/erdpy/cli_cost.py deleted file mode 100644 index 83baa99a..00000000 --- a/erdpy/cli_cost.py +++ /dev/null @@ -1,56 +0,0 @@ -from erdpy.proxy.core import ElrondProxy -from erdpy.proxy.cost import TransactionCostEstimator -import logging -from typing import Any - -from erdpy import cli_contracts, cli_shared, proxy - -logger = logging.getLogger("cli.cost") - - -def setup_parser(subparsers: Any) -> Any: - parser = cli_shared.add_group_subparser(subparsers, "cost", "Estimate cost of Transactions") - subparsers = parser.add_subparsers() - - sub = cli_shared.add_command_subparser(subparsers, "cost", "gas-price", "Query minimum gas price") - cli_shared.add_proxy_arg(sub) - sub.set_defaults(func=get_gas_price) - - sub = cli_shared.add_command_subparser(subparsers, "cost", "tx-transfer", "Query cost of regular transaction (transfer)") - cli_shared.add_proxy_arg(sub) - sub.add_argument("--data", required=True, help="a transaction payload, required to estimate the cost") - sub.set_defaults(func=lambda args: get_transaction_cost(args, proxy.TxTypes.MOVE_BALANCE)) - - sub = cli_shared.add_command_subparser(subparsers, "cost", "sc-deploy", "Query cost of Smart Contract deploy transaction") - cli_shared.add_proxy_arg(sub) - cli_contracts._add_project_or_bytecode_arg(sub) - cli_contracts._add_arguments_arg(sub) - sub.set_defaults(func=lambda args: get_transaction_cost(args, proxy.TxTypes.SC_DEPLOY)) - - sub = cli_shared.add_command_subparser(subparsers, "cost", "sc-call", "Query cost of Smart Contract call transaction") - cli_shared.add_proxy_arg(sub) - cli_contracts._add_contract_arg(sub) - cli_contracts._add_function_arg(sub) - cli_contracts._add_arguments_arg(sub) - sub.set_defaults(func=lambda args: get_transaction_cost(args, proxy.TxTypes.SC_CALL)) - - parser.epilog = cli_shared.build_group_epilog(subparsers) - return subparsers - - -def get_gas_price(args: Any) -> Any: - proxy_url = args.proxy - proxy = ElrondProxy(proxy_url) - price = proxy.get_gas_price() - print(price) - return price - - -def get_transaction_cost(args: Any, tx_type: Any) -> Any: - logger.debug("call_get_transaction_cost") - - cost_estimator = TransactionCostEstimator(args.proxy) - result = cost_estimator.estimate_tx_cost(args, tx_type) - print("Note: gas estimator is deprecated, will be updated on a future release.") - print(result) - return result diff --git a/erdpy/proxy/__init__.py b/erdpy/proxy/__init__.py index 76f68bc8..c8c111f9 100644 --- a/erdpy/proxy/__init__.py +++ b/erdpy/proxy/__init__.py @@ -1,5 +1,3 @@ from erdpy.proxy.core import ElrondProxy -from erdpy.proxy.cost import TransactionCostEstimator -from erdpy.proxy.tx_types import TxTypes -__all__ = ["ElrondProxy", "TransactionCostEstimator", "TxTypes"] +__all__ = ["ElrondProxy"] diff --git a/erdpy/proxy/cost.py b/erdpy/proxy/cost.py deleted file mode 100644 index abf1ca36..00000000 --- a/erdpy/proxy/cost.py +++ /dev/null @@ -1,87 +0,0 @@ -import base64 -import logging - -from erdpy import scope -from erdpy.projects import load_project -from erdpy.proxy.http_facade import do_post -from erdpy.proxy.tx_types import TxTypes - -logger = logging.getLogger("proxy") - - -class TransactionCostEstimator: - # needs these constant because when the observer node receive a post request from proxy with a transaction needs - # to can figure out what type of transaction was send to can calculate an estimation - _SENDER_ADDRESS = "erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz" - _RECEIVER_ADDRESS = "erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r" - - def __init__(self, proxy_url): - self.proxy_url = proxy_url - - def estimate_tx_cost(self, arguments, tx_type): - if tx_type == TxTypes.MOVE_BALANCE: - return self._estimate_move_balance(arguments.data) - elif tx_type == TxTypes.SC_DEPLOY: - return self._estimate_sc_deploy(arguments.project) - else: - return self._estimate_sc_call(arguments.contract, arguments.function, arguments.arguments) - - def _estimate_move_balance(self, data): - sender = self._SENDER_ADDRESS - receiver = self._RECEIVER_ADDRESS - data = data or "" - data_bytes = base64.b64encode(data.encode()) - - estimate = self._send_transaction(sender, receiver, data_bytes.decode()) - return estimate - - def _estimate_sc_deploy(self, contract_path): - if contract_path is None: - logger.fatal("contract-path argument missing") - return - - project = load_project(contract_path) - bytecode = project.get_bytecode() - base64_bytecode = base64.b64encode(bytecode.encode()) - - sender = self._SENDER_ADDRESS - receiver = self._RECEIVER_ADDRESS - estimate = self._send_transaction(sender, receiver, base64_bytecode.decode()) - return estimate - - def _estimate_sc_call(self, sc_address, function, arguments): - if function is None: - logger.fatal("function argument missing") - return - - if sc_address is None: - logger.fatal("sc-address argument missing") - return - - sender = self._SENDER_ADDRESS - receiver = sc_address - arguments = arguments or [] - tx_data = function - for arg in arguments: - # TODO: call prepare_argument() - tx_data += f"@0x0000" - - base64_bytes = base64.b64encode(tx_data.encode()) - estimate = self._send_transaction(sender, receiver, base64_bytes.decode()) - return estimate - - def _send_transaction(self, sender, receiver, data): - tx_object = { - "nonce": 1, - "value": "0", - "receiver": receiver, - "sender": sender, - "data": data, - "chainID": scope.get_chain_id(), - "version": scope.get_tx_version() - } - - url = f"{self.proxy_url}/transaction/cost" - - raw_response = do_post(url, tx_object) - return raw_response.get("txGasUnits", raw_response) diff --git a/erdpy/proxy/tx_types.py b/erdpy/proxy/tx_types.py deleted file mode 100644 index c61d7bb6..00000000 --- a/erdpy/proxy/tx_types.py +++ /dev/null @@ -1,4 +0,0 @@ -class TxTypes: - MOVE_BALANCE = "move-balance" - SC_CALL = "sc-call" - SC_DEPLOY = "sc-deploy" From 17d74f5de3e0d075753db86aaf6f93a2309b51da Mon Sep 17 00:00:00 2001 From: Andrei Bancioiu Date: Fri, 4 Feb 2022 11:17:41 +0200 Subject: [PATCH 03/44] Bit of cleanup. --- erdpy/CLI.md | 94 +----------------------------------- erdpy/CLI.md.sh | 6 --- erdpy/cli.py | 2 - erdpy/tests/test_cli_all.sh | 1 - erdpy/tests/test_cli_cost.sh | 12 ----- 5 files changed, 1 insertion(+), 114 deletions(-) delete mode 100644 erdpy/tests/test_cli_cost.sh diff --git a/erdpy/CLI.md b/erdpy/CLI.md index d871177b..755e37d4 100644 --- a/erdpy/CLI.md +++ b/erdpy/CLI.md @@ -20,7 +20,7 @@ https://docs.elrond.com/sdk-and-tools/erdpy/erdpy. COMMAND GROUPS: - {contract,tx,validator,account,ledger,wallet,network,cost,dispatcher,blockatlas,deps,config,hyperblock,testnet,data,staking-provider,dns} + {contract,tx,validator,account,ledger,wallet,network,dispatcher,blockatlas,deps,config,hyperblock,testnet,data,staking-provider,dns} TOP-LEVEL OPTIONS: -h, --help show this help message and exit @@ -37,7 +37,6 @@ account Get Account data (nonce, balance) from the Networ ledger Get Ledger App addresses and version wallet Create wallet, derive secret key from mnemonic, bech32 address helpers etc. network Get Network parameters, such as number of shards, chain identifier etc. -cost Estimate cost of Transactions dispatcher Enqueue transactions, then bulk dispatch them blockatlas Interact with an Block Atlas instance deps Manage dependencies or elrond-sdk modules @@ -957,97 +956,6 @@ optional arguments: -h, --help show this help message and exit --proxy PROXY 🔗 the URL of the proxy (default: https://testnet-gateway.elrond.com) -``` -## Group **Cost** - - -``` -$ erdpy cost --help -usage: erdpy cost COMMAND [-h] ... - -Estimate cost of Transactions - -COMMANDS: - {gas-price,tx-transfer,sc-deploy,sc-call} - -OPTIONS: - -h, --help show this help message and exit - ----------------- -COMMANDS summary ----------------- -gas-price Query minimum gas price -tx-transfer Query cost of regular transaction (transfer) -sc-deploy Query cost of Smart Contract deploy transaction -sc-call Query cost of Smart Contract call transaction - -``` -### Cost.GasPrice - - -``` -$ erdpy cost gas-price --help -usage: erdpy cost gas-price [-h] ... - -Query minimum gas price - -optional arguments: - -h, --help show this help message and exit - --proxy PROXY 🔗 the URL of the proxy (default: https://testnet-gateway.elrond.com) - -``` -### Cost.TxTransfer - - -``` -$ erdpy cost tx-transfer --help -usage: erdpy cost tx-transfer [-h] ... - -Query cost of regular transaction (transfer) - -optional arguments: - -h, --help show this help message and exit - --proxy PROXY 🔗 the URL of the proxy (default: https://testnet-gateway.elrond.com) - --data DATA a transaction payload, required to estimate the cost - -``` -### Cost.ScDeploy - - -``` -$ erdpy cost sc-deploy --help -usage: erdpy cost sc-deploy [-h] ... - -Query cost of Smart Contract deploy transaction - -optional arguments: - -h, --help show this help message and exit - --proxy PROXY 🔗 the URL of the proxy (default: https://testnet-gateway.elrond.com) - --project PROJECT 🗀 the project directory (default: current directory) - --bytecode BYTECODE the file containing the WASM bytecode - --arguments ARGUMENTS [ARGUMENTS ...] arguments for the contract transaction, as numbers or hex-encoded. E.g. - --arguments 42 0x64 1000 0xabba - -``` -### Cost.ScCall - - -``` -$ erdpy cost sc-call --help -usage: erdpy cost sc-call [-h] ... - -Query cost of Smart Contract call transaction - -positional arguments: - contract 🖄 the address of the Smart Contract - -optional arguments: - -h, --help show this help message and exit - --proxy PROXY 🔗 the URL of the proxy (default: https://testnet-gateway.elrond.com) - --function FUNCTION the function to call - --arguments ARGUMENTS [ARGUMENTS ...] arguments for the contract transaction, as numbers or hex-encoded. E.g. - --arguments 42 0x64 1000 0xabba - ``` ## Group **Dispatcher** diff --git a/erdpy/CLI.md.sh b/erdpy/CLI.md.sh index d78d0a11..6adb21ea 100755 --- a/erdpy/CLI.md.sh +++ b/erdpy/CLI.md.sh @@ -84,12 +84,6 @@ generate() { command "Network.BlockNonce" "network block-nonce" command "Network.Chain" "network chain" - group "Cost" "cost" - command "Cost.GasPrice" "cost gas-price" - command "Cost.TxTransfer" "cost tx-transfer" - command "Cost.ScDeploy" "cost sc-deploy" - command "Cost.ScCall" "cost sc-call" - group "Dispatcher" "dispatcher" command "Dispatcher.Enqueue" "dispatcher enqueue" command "Dispatcher.Dispatch" "dispatcher dispatch" diff --git a/erdpy/cli.py b/erdpy/cli.py index ec3a1bdb..72c2987c 100644 --- a/erdpy/cli.py +++ b/erdpy/cli.py @@ -9,7 +9,6 @@ import erdpy.cli_blockatlas import erdpy.cli_config import erdpy.cli_contracts -import erdpy.cli_cost import erdpy.cli_data import erdpy.cli_deps import erdpy.cli_dispatcher @@ -91,7 +90,6 @@ def setup_parser(args: List[str] = sys.argv[1:]): commands.append(erdpy.cli_ledger.setup_parser(subparsers)) commands.append(erdpy.cli_wallet.setup_parser(args, subparsers)) commands.append(erdpy.cli_network.setup_parser(subparsers)) - commands.append(erdpy.cli_cost.setup_parser(subparsers)) commands.append(erdpy.cli_dispatcher.setup_parser(args, subparsers)) commands.append(erdpy.cli_blockatlas.setup_parser(subparsers)) commands.append(erdpy.cli_deps.setup_parser(subparsers)) diff --git a/erdpy/tests/test_cli_all.sh b/erdpy/tests/test_cli_all.sh index bcad751c..3788c185 100644 --- a/erdpy/tests/test_cli_all.sh +++ b/erdpy/tests/test_cli_all.sh @@ -15,7 +15,6 @@ testAll() { source ./test_cli_tx.sh && testAll source ./test_cli_config.sh && testAll source ./test_cli_network.sh && testAll - source ./test_cli_cost.sh && testAll fi popd } diff --git a/erdpy/tests/test_cli_cost.sh b/erdpy/tests/test_cli_cost.sh deleted file mode 100644 index 41eec669..00000000 --- a/erdpy/tests/test_cli_cost.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -source "./shared.sh" - -testAll() { - set -x - - ${ERDPY} --verbose cost gas-price --proxy=${PROXY} - ${ERDPY} --verbose cost tx-transfer --data="foobar" --proxy=${PROXY} - - set +x -} From 5a8b02d5602d54d364bec881a7b69cbf73dbd949 Mon Sep 17 00:00:00 2001 From: Andrei Bancioiu Date: Fri, 4 Feb 2022 16:55:59 +0200 Subject: [PATCH 04/44] CLI tests improvements. --- erdpy/tests/shared.sh | 2 +- erdpy/tests/test_cli_tx.sh | 21 +++++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/erdpy/tests/shared.sh b/erdpy/tests/shared.sh index 83f13368..366243a0 100755 --- a/erdpy/tests/shared.sh +++ b/erdpy/tests/shared.sh @@ -8,7 +8,7 @@ export PYTHONPATH=$(absolute_path ../../) echo "PYTHONPATH = ${PYTHONPATH}" ERDPY="python3.8 -m erdpy.cli" -SANDBOX=testdata-out/SANDBOX +SANDBOX=./testdata-out/SANDBOX USERS=../testnet/wallets/users VALIDATORS=../testnet/wallets/validators DENOMINATION="000000000000000000" diff --git a/erdpy/tests/test_cli_tx.sh b/erdpy/tests/test_cli_tx.sh index cebbd19c..97ad5faa 100644 --- a/erdpy/tests/test_cli_tx.sh +++ b/erdpy/tests/test_cli_tx.sh @@ -5,20 +5,25 @@ source "./shared.sh" BOB="erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r" testAll() { + cleanSandbox || return 1 + echo "tx new, don't --send" - ${ERDPY} --verbose tx new --pem="${USERS}/alice.pem" --receiver=${BOB} --value="1${DENOMINATION}" --nonce=42 --data="foo" --gas-limit=70000 --chain=${CHAIN_ID} --outfile=${SANDBOX}/tx42.txt + ${ERDPY} --verbose tx new --pem="${USERS}/alice.pem" --receiver=${BOB} --value="1${DENOMINATION}" --nonce=42 --data="foo" --gas-limit=70000 --chain=${CHAIN_ID} --outfile=${SANDBOX}/tx42.txt || return 1 echo "tx send" - ${ERDPY} --verbose tx send --infile=${SANDBOX}/tx42.txt --proxy=${PROXY} + ${ERDPY} --verbose tx send --infile=${SANDBOX}/tx42.txt --proxy=${PROXY} || return 1 echo "tx new --send" - ${ERDPY} --verbose tx new --pem="${USERS}/alice.pem" --receiver=${BOB} --value="1${DENOMINATION}" --nonce=43 --data="foo" --gas-limit=60000 --chain=${CHAIN_ID} --send --outfile=${SANDBOX}/tx43.txt --proxy=${PROXY} + ${ERDPY} --verbose tx new --pem="${USERS}/alice.pem" --receiver=${BOB} --value="1${DENOMINATION}" --nonce=43 --data="foo" --gas-limit=60000 --chain=${CHAIN_ID} --send --outfile=${SANDBOX}/tx43.txt --proxy=${PROXY} || return 1 echo "tx new with --data-file" echo '"{hello world!}"' > ${SANDBOX}/dummy.txt - ${ERDPY} --verbose tx new --pem="${USERS}/alice.pem" --receiver=${BOB} --value="1${DENOMINATION}" --recall-nonce --data-file=${SANDBOX}/dummy.txt --gas-limit=70000 --chain=${CHAIN_ID} --proxy=${PROXY} + ${ERDPY} --verbose tx new --pem="${USERS}/alice.pem" --receiver=${BOB} --value="1${DENOMINATION}" --recall-nonce --data-file=${SANDBOX}/dummy.txt --gas-limit=70000 --chain=${CHAIN_ID} --proxy=${PROXY} || return 1 echo "tx new --relay" - ${ERDPY} --verbose tx new --pem="${USERS}/carol.pem" --receiver=${BOB} --value="1${DENOMINATION}" --nonce=1 --data="foo" --gas-limit=70000 --chain=${CHAIN_ID} --outfile=${SANDBOX}/txInner.txt --relay - ${ERDPY} --verbose tx new --pem="${USERS}/alice.pem" --receiver=${BOB} --value="1${DENOMINATION}" --nonce=44 --data-file=${SANDBOX}/txInner.txt --gas-limit=200000 --chain=${CHAIN_ID} --outfile=${SANDBOX}/txWrapper.txt + ${ERDPY} --verbose tx new --pem="${USERS}/carol.pem" --receiver=${BOB} --value="1${DENOMINATION}" --nonce=1 --data="foo" --gas-limit=70000 --chain=${CHAIN_ID} --outfile=${SANDBOX}/txInner.txt --relay || return 1 + ${ERDPY} --verbose tx new --pem="${USERS}/alice.pem" --receiver=${BOB} --value="1${DENOMINATION}" --nonce=44 --data-file=${SANDBOX}/txInner.txt --gas-limit=200000 --chain=${CHAIN_ID} --outfile=${SANDBOX}/txWrapper.txt || return 1 echo "tx new --simulate" - ${ERDPY} --verbose tx new --simulate --pem="${USERS}/alice.pem" --receiver=${BOB} --value="1${DENOMINATION}" --nonce=42 --data="foo" --gas-limit=70000 --chain=${CHAIN_ID} --proxy=${PROXY} --outfile=${SANDBOX}/tx42.txt -} \ No newline at end of file + ${ERDPY} --verbose tx new --simulate --pem="${USERS}/alice.pem" --receiver=${BOB} --value="1${DENOMINATION}" --recall-nonce --data="foo" --gas-limit=70000 --chain=${CHAIN_ID} --proxy=${PROXY} --outfile=${SANDBOX}/tx42.txt || return 1 + + echo "tx new --send --wait-result" + ${ERDPY} --verbose tx new --send --wait-result --pem="${USERS}/alice.pem" --receiver=${BOB} --value="1${DENOMINATION}" --recall-nonce --data="foo" --gas-limit=70000 --chain=${CHAIN_ID} --proxy=${PROXY} || return 1 +} From 18c8bf1dce6d45ba08ff1a221c5981bb3ba5a26b Mon Sep 17 00:00:00 2001 From: Andrei Bancioiu Date: Fri, 4 Feb 2022 16:58:09 +0200 Subject: [PATCH 05/44] More explicit types on proxy endpoints. --- erdpy/interfaces.py | 28 ++++++++-- erdpy/proxy/core.py | 26 ++++----- erdpy/proxy/http_facade.py | 13 +++-- erdpy/proxy/messages.py | 105 ++++++++++++++++++++++++++++++++++++- erdpy/utils.py | 21 +++++--- 5 files changed, 164 insertions(+), 29 deletions(-) diff --git a/erdpy/interfaces.py b/erdpy/interfaces.py index 84e8dec6..200d7000 100644 --- a/erdpy/interfaces.py +++ b/erdpy/interfaces.py @@ -1,4 +1,5 @@ from typing import Any, Dict, List, Tuple +from erdpy.utils import ISerializable class IAddress: @@ -37,21 +38,40 @@ def sign_transaction(self, transaction: ITransaction) -> str: return "" +class ITransactionOnNetwork(ISerializable): + def is_done(self) -> bool: + return False + + +class ISimulateResponse(ISerializable): + pass + + +class ISimulateCostResponse(ISerializable): + pass + + class IElrondProxy: def get_account_nonce(self, address: IAddress) -> int: return 0 + def get_transaction(self, tx_hash: str, sender_address: str = "", with_results: bool = False) -> ITransactionOnNetwork: + return ITransactionOnNetwork() + def send_transaction(self, payload: Any) -> str: return "" - def simulate_transaction(self, payload: Any) -> str: - return "" + def simulate_transaction(self, payload: Any) -> ISimulateResponse: + return ISimulateResponse() + + def simulate_transaction_cost(self, payload: Any) -> ISimulateCostResponse: + return ISimulateCostResponse() def send_transactions(self, payload: List[Any]) -> Tuple[int, List[str]]: return 0, [] - def send_transaction_and_wait_for_result(self, payload: Any, num_seconds_timeout: int) -> Dict[str, Any]: - return dict() + def send_transaction_and_wait_for_result(self, payload: Any, num_seconds_timeout: int) -> ITransactionOnNetwork: + return ITransactionOnNetwork() def query_contract(self, payload: Any) -> Any: return dict() diff --git a/erdpy/proxy/core.py b/erdpy/proxy/core.py index f696bc42..9bff45a6 100644 --- a/erdpy/proxy/core.py +++ b/erdpy/proxy/core.py @@ -1,11 +1,11 @@ import logging import time -from typing import Any, List, Tuple, cast, Dict +from typing import Any, List, Tuple, cast from erdpy.accounts import Address -from erdpy.interfaces import IAddress, IElrondProxy +from erdpy.interfaces import IAddress, IElrondProxy, ISimulateCostResponse, ISimulateResponse, ITransactionOnNetwork from erdpy.proxy.http_facade import do_get, do_post -from erdpy.proxy.messages import NetworkConfig +from erdpy.proxy.messages import NetworkConfig, SimulateCostResponse, SimulateResponse, TransactionOnNetwork METACHAIN_ID = 4294967295 ANY_SHARD_ID = 0 @@ -102,10 +102,15 @@ def send_transaction(self, payload: Any) -> str: tx_hash = str(response.get("txHash")) return tx_hash - def simulate_transaction(self, payload: Any) -> str: + def simulate_transaction(self, payload: Any) -> ISimulateResponse: url = f"{self.url}/transaction/simulate" - response = str(do_post(url, payload)) - return response + response = do_post(url, payload) + return SimulateResponse(response) + + def simulate_transaction_cost(self, payload: Any) -> ISimulateCostResponse: + url = f"{self.url}/transaction/cost" + response = do_post(url, payload) + return SimulateCostResponse(response) def send_transactions(self, payload: List[Any]) -> Tuple[int, List[str]]: url = f"{self.url}/transaction/send-multiple" @@ -120,16 +125,13 @@ def query_contract(self, payload: Any) -> Any: response = do_post(url, payload) return response - def get_transaction(self, tx_hash: str, sender_address: str = "", with_results: bool = False) -> Dict[str, Any]: + def get_transaction(self, tx_hash: str, sender_address: str = "", with_results: bool = False) -> ITransactionOnNetwork: url = f"{self.url}/transaction/{tx_hash}" url += f"?sender={sender_address or ''}" url += f"&withResults={with_results}" response = do_get(url) - transaction_response = response.get("transaction", dict()) - transaction = cast(Dict[str, Any], transaction_response) - transaction['hash'] = tx_hash - return transaction + return TransactionOnNetwork(tx_hash, response) def get_hyperblock(self, key) -> Any: url = f"{self.url}/hyperblock/by-hash/{key}" @@ -140,7 +142,7 @@ def get_hyperblock(self, key) -> Any: response = response.get("hyperblock", {}) return response - def send_transaction_and_wait_for_result(self, payload: Any, num_seconds_timeout: int = 100) -> Dict[str, Any]: + def send_transaction_and_wait_for_result(self, payload: Any, num_seconds_timeout: int = 100) -> ITransactionOnNetwork: url = f"{self.url}/transaction/send" response = do_post(url, payload) tx_hash = response.get("txHash") diff --git a/erdpy/proxy/http_facade.py b/erdpy/proxy/http_facade.py index e40d2807..02aab53b 100644 --- a/erdpy/proxy/http_facade.py +++ b/erdpy/proxy/http_facade.py @@ -1,8 +1,10 @@ +from typing import Any, Dict import requests from erdpy import errors +from erdpy.proxy.messages import GenericProxyResponse -def do_get(url): +def do_get(url: str) -> GenericProxyResponse: try: response = requests.get(url) response.raise_for_status() @@ -17,7 +19,7 @@ def do_get(url): raise errors.ProxyRequestError(url, err) -def do_post(url, payload): +def do_post(url: str, payload: Any) -> GenericProxyResponse: try: response = requests.post(url, json=payload) response.raise_for_status() @@ -32,17 +34,18 @@ def do_post(url, payload): raise errors.ProxyRequestError(url, err) -def get_data(parsed, url): +def get_data(parsed: Dict[str, Any], url: str) -> GenericProxyResponse: err = parsed.get("error") code = parsed.get("code") if not err and code == "successful": - return parsed.get("data", dict()) + data: Dict[str, Any] = parsed.get("data", dict()) + return GenericProxyResponse(data) raise errors.ProxyRequestError(url, f"code:{code}, error: {err}") -def _extract_error_from_response(response): +def _extract_error_from_response(response: Any): try: return response.json() except Exception: diff --git a/erdpy/proxy/messages.py b/erdpy/proxy/messages.py index b0d2f54f..0c483a0b 100644 --- a/erdpy/proxy/messages.py +++ b/erdpy/proxy/messages.py @@ -1,4 +1,16 @@ -from typing import Any, Dict +import base64 +from typing import Any, Dict, List, Union + +from erdpy.interfaces import ISimulateCostResponse, ISimulateResponse, ITransactionOnNetwork +from erdpy.utils import ISerializable + + +class GenericProxyResponse(ISerializable): + def __init__(self, data: Any) -> None: + self.__dict__.update(data) + + def get(self, key: str, default: Any = None) -> Any: + return self.__dict__.get(key, default) class NetworkConfig: @@ -7,3 +19,94 @@ def __init__(self, data: Dict[str, Any]) -> None: self.min_gas_price = data.get("erd_min_gas_price", 0) self.chain_id = data.get("erd_chain_id", "?") self.min_tx_version = data.get("erd_min_transaction_version", 0) + + +class TransactionOnNetwork(ITransactionOnNetwork): + def __init__(self, hash: str, response: GenericProxyResponse) -> None: + raw = response.get("transaction", dict()) + contract_results: List[Dict[str, Any]] = raw.get("smartContractResults", []) + logs: List[Dict[str, Any]] = raw.get("logs", []) + + self.raw = raw + self.hash = hash + self.parsed_contract_results = [SmartContractResult(item) for item in contract_results] + self.parsed_logs = [Log(item) for item in logs] + + def is_done(self) -> bool: + return self.raw.get("hyperblockNonce", 0) > 0 + + def to_dictionary(self) -> Dict[str, Any]: + result: Dict[str, Any] = dict() + result.update(self.raw) + result["hash"] = self.hash + result["parsed"] = { + "smartContractResults": self.parsed_contract_results, + "logs": self.parsed_logs + } + + return result + + +class SmartContractResult(ISerializable): + def __init__(self, raw: Dict[str, Any]) -> None: + self.raw = raw + self.parsed_data = decode_hex_base64(raw.get("data")) + self.parsed_log = Log(raw.get("logs", {})) + + def to_dictionary(self) -> Dict[str, Any]: + result: Dict[str, Any] = dict() + result.update(self.raw) + result["parsed"] = { + "data": self.parsed_data, + "log": self.parsed_log + } + + return result + + +class Log(ISerializable): + def __init__(self, raw: Dict[str, Any]) -> None: + self.events = [] + + +class SimulateResponse(ISimulateResponse): + def __init__(self, response: GenericProxyResponse) -> None: + contract_results: Dict[str, Any] = response.get("scResults") or dict() + + self.raw = response.to_dictionary() + self.parsed_contract_results = [SmartContractResult(item) for item in contract_results.values()] + + def to_dictionary(self) -> Dict[str, Any]: + result: Dict[str, Any] = dict() + result.update(self.raw) + result["parsed"] = { + "smartContractResults": self.parsed_contract_results + } + + return result + + +class SimulateCostResponse(ISimulateCostResponse): + def __init__(self, response: GenericProxyResponse) -> None: + contract_results: Dict[str, Any] = response.get("smartContractResults") or dict() + + self.raw = response.to_dictionary() + self.parsed_contract_results = [SmartContractResult(item) for item in contract_results.values()] + + def to_dictionary(self) -> Dict[str, Any]: + result: Dict[str, Any] = dict() + result.update(self.raw) + result["parsed"] = { + "smartContractResults": self.parsed_contract_results + } + + return result + + +def decode_hex_base64(input: Union[str, None]) -> str: + return bytes.fromhex(decode_base64(input)).decode('ascii') if input else "" + + +def decode_base64(input: Union[str, None]) -> str: + return base64.b64decode(input).decode() if input else "" + diff --git a/erdpy/utils.py b/erdpy/utils.py index ee1d2cbb..e25af9bb 100644 --- a/erdpy/utils.py +++ b/erdpy/utils.py @@ -18,21 +18,28 @@ logger = logging.getLogger("utils") +class ISerializable: + def to_dictionary(self) -> Dict[str, Any]: + return self.__dict__ -class Object: + +class Object(ISerializable): def __repr__(self): return str(self.__dict__) + def to_dictionary(self): + return dict(self.__dict__) + def to_json(self): data_json = json.dumps(self.__dict__, indent=4) return data_json -class ObjectEncoder(json.JSONEncoder): - def default(self, obj): - if isinstance(obj, Object): - return obj.__dict__ - return json.JSONEncoder.default(self, obj) +class BasicEncoder(json.JSONEncoder): + def default(self, o: Any): + if isinstance(o, ISerializable): + return o.to_dictionary() + return json.JSONEncoder.default(self, o) def omit_fields(data: Any, fields: List[str] = []): @@ -147,7 +154,7 @@ def dump_out_json(data: Any, outfile: Any = None): if not outfile: outfile = sys.stdout - json.dump(data, outfile, indent=4, cls=ObjectEncoder) + json.dump(data, outfile, indent=4, cls=BasicEncoder) outfile.write("\n") From 46acfc9c5099b3d7e6a2ff0408ae3ea9edda6f09 Mon Sep 17 00:00:00 2001 From: Andrei Bancioiu Date: Fri, 4 Feb 2022 16:58:39 +0200 Subject: [PATCH 06/44] Optimize tx --wait-result (look for hyperblockNonce instead of querying whole hyperblocks). --- erdpy/proxy/core.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/erdpy/proxy/core.py b/erdpy/proxy/core.py index 9bff45a6..8e29c69b 100644 --- a/erdpy/proxy/core.py +++ b/erdpy/proxy/core.py @@ -148,18 +148,11 @@ def send_transaction_and_wait_for_result(self, payload: Any, num_seconds_timeout tx_hash = response.get("txHash") for _ in range(0, num_seconds_timeout): - if self.is_transaction_finalized(tx_hash): - return self.get_transaction(tx_hash=tx_hash, with_results=True) time.sleep(1) - return dict() - def is_transaction_finalized(self, tx_hash): - last_nonce = self.get_last_block_nonce("metachain") - last_hyperblock = self.get_hyperblock(last_nonce) - finalized_transactions = last_hyperblock["transactions"] + tx = self.get_transaction(tx_hash=tx_hash, with_results=True) + if tx.is_done(): + return tx - for transaction in finalized_transactions: - if transaction["hash"] == tx_hash: - return True - - return False + return ITransactionOnNetwork() + From db1e634fcc752dc5631e421a25b39bb5d7d08144 Mon Sep 17 00:00:00 2001 From: Andrei Bancioiu Date: Sat, 5 Feb 2022 23:22:58 +0200 Subject: [PATCH 07/44] TX simulations; redesign CLI output. --- erdpy/cli_contracts.py | 72 ++++++++++++-------------------------- erdpy/cli_shared.py | 10 +++--- erdpy/cli_transactions.py | 23 ++++++------ erdpy/dns.py | 11 +++--- erdpy/interfaces.py | 3 ++ erdpy/proxy/messages.py | 3 ++ erdpy/simulation.py | 29 +++++++++++++++ erdpy/tests/test_cli_tx.sh | 2 +- erdpy/transactions.py | 32 ++++++----------- 9 files changed, 95 insertions(+), 90 deletions(-) create mode 100644 erdpy/simulation.py diff --git a/erdpy/cli_contracts.py b/erdpy/cli_contracts.py index 5f0fbc2b..4c726a11 100644 --- a/erdpy/cli_contracts.py +++ b/erdpy/cli_contracts.py @@ -1,6 +1,7 @@ +from collections import OrderedDict import logging import os -from typing import Any, List, Union +from typing import Any, Dict, List from pathlib import Path @@ -9,6 +10,7 @@ from erdpy.contracts import CodeMetadata, SmartContract from erdpy.projects import load_project from erdpy.proxy.core import ElrondProxy +from erdpy.simulation import Simulator from erdpy.transactions import Transaction logger = logging.getLogger("cli.contracts") @@ -211,36 +213,8 @@ def deploy(args: Any): logger.info("Contract address: %s", contract.address) utils.log_explorer_contract_address(chain, contract.address) - result = None - try: - result = _send_or_simulate(tx, args) - finally: - txdict = tx.to_dump_dict() - txdict['address'] = contract.address.bech32() - dump_tx_and_result(txdict, result, args) - - -def dump_tx_and_result(tx: Any, result: Any, args: Any): - dump = dict() - dump['emitted_tx'] = tx - - try: - returnCode = '' - returnMessage = '' - dump['result'] = result['result'] - for scrHash, scr in dump['result']['scResults'].items(): - if scr['receiver'] == tx['tx']['sender']: - retCode = scr['data'][1:] - retCode = bytes.fromhex(retCode).decode('ascii') - returnCode = retCode - returnMessage = scr['returnMessage'] - - dump['result']['returnCode'] = returnCode - dump['result']['returnMessage'] = returnMessage - except TypeError: - pass - - utils.dump_out_json(dump, outfile=args.outfile) + output = _send_or_simulate(tx, contract, args) + utils.dump_out_json(output, outfile=args.outfile) def _prepare_contract(args: Any) -> SmartContract: @@ -292,11 +266,8 @@ def call(args: Any): sender = _prepare_sender(args) tx = contract.execute(sender, function, arguments, gas_price, gas_limit, value, chain, version) - try: - result = _send_or_simulate(tx, args) - finally: - txdict = tx.to_dump_dict() - dump_tx_and_result(txdict, result, args) + output = _send_or_simulate(tx, contract, args) + utils.dump_out_json(output, outfile=args.outfile) def upgrade(args: Any): @@ -316,11 +287,8 @@ def upgrade(args: Any): sender = _prepare_sender(args) tx = contract.upgrade(sender, arguments, gas_price, gas_limit, value, chain, version) - try: - result = _send_or_simulate(tx, args) - finally: - txdict = tx.to_dump_dict() - dump_tx_and_result(txdict, result, args) + output = _send_or_simulate(tx, contract, args) + utils.dump_out_json(output, outfile=args.outfile) def query(args: Any): @@ -335,18 +303,24 @@ def query(args: Any): utils.dump_out_json(result) -def _send_or_simulate(tx: Transaction, args: Any): +def _send_or_simulate(tx: Transaction, contract: SmartContract, args: Any) -> Dict[str, Any]: + proxy = ElrondProxy(args.proxy) + send_wait_result = args.wait_result and args.send and not args.simulate send_only = args.send and not (args.wait_result or args.simulate) simulate = args.simulate and not (send_only or send_wait_result) + output: OrderedDict[str, Any] = OrderedDict() + if send_wait_result: - proxy = ElrondProxy(args.proxy) - response = tx.send_wait_result(proxy, args.timeout) - return None + output["txOnNetwork"] = tx.send_wait_result(proxy, args.timeout) elif send_only: - tx.send(ElrondProxy(args.proxy)) - return None + tx.send(proxy) elif simulate: - response = tx.simulate(ElrondProxy(args.proxy)) - return response + output["txSimulation"] = Simulator(proxy).run(tx) + + # For backwards compatibility (a lot of interaction scripts rely on this attributes): + output["emitted_tx"] = tx.to_dump_dict() + output["emitted_tx"]["address"] = contract.address.bech32() + + return output diff --git a/erdpy/cli_shared.py b/erdpy/cli_shared.py index d9604d83..e7afa121 100644 --- a/erdpy/cli_shared.py +++ b/erdpy/cli_shared.py @@ -8,6 +8,7 @@ from erdpy.accounts import Account from erdpy.ledger.ledger_functions import do_get_ledger_address from erdpy.proxy.core import ElrondProxy +from erdpy.simulation import Simulator from erdpy.transactions import Transaction @@ -124,7 +125,7 @@ def prepare_nonce_in_args(args: Any): args.nonce = account.nonce -def add_broadcast_args(sub: Any, simulate=True, relay=False): +def add_broadcast_args(sub: Any, simulate: bool = True, relay: bool = False): sub.add_argument("--send", action="store_true", default=False, help="✓ whether to broadcast the transaction (default: %(default)s)") if simulate: @@ -141,11 +142,12 @@ def check_broadcast_args(args: Any): def send_or_simulate(tx: Transaction, args: Any): + proxy = ElrondProxy(args.proxy) if args.send: - tx.send(ElrondProxy(args.proxy)) + tx.send(proxy) elif args.simulate: - response = tx.simulate(ElrondProxy(args.proxy)) - utils.dump_out_json(response) + simulation = Simulator(proxy).run(tx) + utils.dump_out_json(simulation) def check_if_sign_method_required(args: List[str], checked_method: str) -> bool: diff --git a/erdpy/cli_transactions.py b/erdpy/cli_transactions.py index e4dc088c..6300e7e2 100644 --- a/erdpy/cli_transactions.py +++ b/erdpy/cli_transactions.py @@ -1,8 +1,9 @@ from argparse import FileType -from typing import Any, List +from typing import Any, Dict, List from erdpy import cli_shared, utils from erdpy.proxy.core import ElrondProxy +from erdpy.simulation import Simulator from erdpy.transactions import Transaction, do_prepare_transaction @@ -64,18 +65,19 @@ def create_transaction(args: Any): send_only = args.send and not (args.wait_result or args.simulate) simulate = args.simulate and not (send_only or send_wait_result) + proxy = ElrondProxy(args.proxy) + output: Dict[str, Any] = dict() + try: if send_wait_result: - proxy = ElrondProxy(args.proxy) - response = tx.send_wait_result(proxy, args.timeout) - utils.dump_out_json(response) + output["txOnNetwork"] = tx.send_wait_result(proxy, args.timeout) elif send_only: - tx.send(ElrondProxy(args.proxy)) + tx.send(proxy) elif simulate: - response = tx.simulate(ElrondProxy(args.proxy)) - utils.dump_out_json(response) + output["txSimulation"] = Simulator(proxy).run(tx) finally: - tx.dump_to(args.outfile) + output.update(tx.to_dump_dict()) + utils.dump_out_json(output, outfile=args.outfile) def send_transaction(args: Any): @@ -96,5 +98,6 @@ def get_transaction(args: Any): proxy = ElrondProxy(args.proxy) transaction = proxy.get_transaction(args.hash, args.sender, args.with_results) - utils.omit_fields(transaction, omit_fields) - utils.dump_out_json(transaction) + transaction_dictionary = transaction.to_dictionary() + utils.omit_fields(transaction_dictionary, omit_fields) + utils.dump_out_json(transaction_dictionary) diff --git a/erdpy/dns.py b/erdpy/dns.py index 5d94ed18..e6ec0421 100644 --- a/erdpy/dns.py +++ b/erdpy/dns.py @@ -6,8 +6,9 @@ from erdpy.accounts import Account, Address from erdpy.contracts import SmartContract from erdpy.proxy.core import ElrondProxy +from erdpy.simulation import Simulator from erdpy.transactions import do_prepare_transaction -from erdpy.interfaces import IElrondProxy, ITransaction +from erdpy.interfaces import IElrondProxy MaxNumShards = 256 ShardIdentiferLen = 2 @@ -45,12 +46,14 @@ def register(args: Any): args.outfile.write(tx.serialize_as_inner()) return + proxy = ElrondProxy(args.proxy) + try: if args.send: - tx.send(ElrondProxy(args.proxy)) + tx.send(proxy) elif args.simulate: - response = tx.simulate(ElrondProxy(args.proxy)) - utils.dump_out_json(response) + simulation = Simulator(proxy).run(tx) + utils.dump_out_json(simulation) finally: tx.dump_to(args.outfile) diff --git a/erdpy/interfaces.py b/erdpy/interfaces.py index 200d7000..f6976022 100644 --- a/erdpy/interfaces.py +++ b/erdpy/interfaces.py @@ -42,6 +42,9 @@ class ITransactionOnNetwork(ISerializable): def is_done(self) -> bool: return False + def get_hash(self) -> str: + return "" + class ISimulateResponse(ISerializable): pass diff --git a/erdpy/proxy/messages.py b/erdpy/proxy/messages.py index 0c483a0b..82c54819 100644 --- a/erdpy/proxy/messages.py +++ b/erdpy/proxy/messages.py @@ -35,6 +35,9 @@ def __init__(self, hash: str, response: GenericProxyResponse) -> None: def is_done(self) -> bool: return self.raw.get("hyperblockNonce", 0) > 0 + def get_hash(self) -> str: + return self.hash + def to_dictionary(self) -> Dict[str, Any]: result: Dict[str, Any] = dict() result.update(self.raw) diff --git a/erdpy/simulation.py b/erdpy/simulation.py new file mode 100644 index 00000000..b1a345b8 --- /dev/null +++ b/erdpy/simulation.py @@ -0,0 +1,29 @@ +from collections import OrderedDict +from typing import Any, Dict +from erdpy.interfaces import IElrondProxy, ISimulateCostResponse, ISimulateResponse, ITransaction +from erdpy.utils import ISerializable + + +class Simulation(ISerializable): + def __init__(self, simulate_response: ISimulateResponse, simulate_cost_response: ISimulateCostResponse) -> None: + self.simulation_response = simulate_response + self.cost_simulation_response = simulate_cost_response + + def to_dictionary(self) -> Dict[str, Any]: + dictionary: Dict[str, Any] = OrderedDict() + dictionary["execution"] = self.simulation_response.to_dictionary() + dictionary["cost"] = self.cost_simulation_response.to_dictionary() + + return dictionary + +class Simulator(): + def __init__(self, proxy: IElrondProxy) -> None: + self.proxy = proxy + + def run(self, transaction: ITransaction) -> Simulation: + dictionary = transaction.to_dictionary() + simulation_response = self.proxy.simulate_transaction(dictionary) + cost_simulation_response = self.proxy.simulate_transaction_cost(dictionary) + + return Simulation(simulation_response, cost_simulation_response) + diff --git a/erdpy/tests/test_cli_tx.sh b/erdpy/tests/test_cli_tx.sh index 97ad5faa..e082e29f 100644 --- a/erdpy/tests/test_cli_tx.sh +++ b/erdpy/tests/test_cli_tx.sh @@ -22,7 +22,7 @@ testAll() { ${ERDPY} --verbose tx new --pem="${USERS}/alice.pem" --receiver=${BOB} --value="1${DENOMINATION}" --nonce=44 --data-file=${SANDBOX}/txInner.txt --gas-limit=200000 --chain=${CHAIN_ID} --outfile=${SANDBOX}/txWrapper.txt || return 1 echo "tx new --simulate" - ${ERDPY} --verbose tx new --simulate --pem="${USERS}/alice.pem" --receiver=${BOB} --value="1${DENOMINATION}" --recall-nonce --data="foo" --gas-limit=70000 --chain=${CHAIN_ID} --proxy=${PROXY} --outfile=${SANDBOX}/tx42.txt || return 1 + ${ERDPY} --verbose tx new --simulate --pem="${USERS}/alice.pem" --receiver=${BOB} --value="1${DENOMINATION}" --recall-nonce --data="foo" --gas-limit=70000 --chain=${CHAIN_ID} --proxy=${PROXY} || return 1 echo "tx new --send --wait-result" ${ERDPY} --verbose tx new --send --wait-result --pem="${USERS}/alice.pem" --receiver=${BOB} --value="1${DENOMINATION}" --recall-nonce --data="foo" --gas-limit=70000 --chain=${CHAIN_ID} --proxy=${PROXY} || return 1 diff --git a/erdpy/transactions.py b/erdpy/transactions.py index f620b0f2..0b80d98e 100644 --- a/erdpy/transactions.py +++ b/erdpy/transactions.py @@ -2,11 +2,11 @@ import json import logging from collections import OrderedDict -from typing import Any, Dict, List, TextIO, Union +from typing import Any, Dict, List, TextIO from erdpy import config, errors, utils from erdpy.accounts import Account, Address, LedgerAccount -from erdpy.interfaces import IElrondProxy, ITransaction +from erdpy.interfaces import IElrondProxy, ITransaction, ITransactionOnNetwork logger = logging.getLogger("transactions") @@ -90,21 +90,16 @@ def load_from_file(cls, f: TextIO): instance.receiverUsername = instance.receiver_username_encoded() return instance - def to_dump_dict(self, extra: Any = {}): + def to_dump_dict(self): dump_dict: Dict[str, Any] = dict() dump_dict['tx'] = self.to_dictionary() dump_dict['hash'] = self.hash or "" dump_dict['data'] = self.data - dump_dict.update(extra) return dump_dict - def dump_to(self, f: Any, extra: Any = {}): - dump: Any = utils.Object() - dump.tx = self.to_dictionary() - dump.hash = self.hash or "" - dump.data = self.data - dump.__dict__.update(extra) - f.writelines([dump.to_json(), "\n"]) + def dump_to(self, f: Any): + dump_dict: Any = self.to_dump_dict() + utils.dump_out_json(dump_dict, f) def send(self, proxy: IElrondProxy): if not self.signature: @@ -118,20 +113,13 @@ def send(self, proxy: IElrondProxy): utils.log_explorer_transaction(self.chainID, self.hash) return self.hash - def send_wait_result(self, proxy, timeout): + def send_wait_result(self, proxy: IElrondProxy, timeout: int) -> ITransactionOnNetwork: if not self.signature: raise errors.TransactionIsNotSigned() - response = proxy.send_transaction_and_wait_for_result(self.to_dictionary(), timeout) - self.hash = response['hash'] - return response - - def simulate(self, proxy: IElrondProxy): - if not self.signature: - raise errors.TransactionIsNotSigned() - - dictionary = self.to_dictionary() - return proxy.simulate_transaction(dictionary) + txOnNetwork = proxy.send_transaction_and_wait_for_result(self.to_dictionary(), timeout) + self.hash = txOnNetwork.get_hash() + return txOnNetwork def to_dictionary(self) -> Dict[str, Any]: dictionary: Dict[str, Any] = OrderedDict() From 73136f062f186fa0efeec733c8c0576adbd0b863 Mon Sep 17 00:00:00 2001 From: Andrei Bancioiu Date: Sat, 5 Feb 2022 23:23:18 +0200 Subject: [PATCH 08/44] Extra logging. --- erdpy/proxy/core.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erdpy/proxy/core.py b/erdpy/proxy/core.py index 8e29c69b..e71859e9 100644 --- a/erdpy/proxy/core.py +++ b/erdpy/proxy/core.py @@ -153,6 +153,8 @@ def send_transaction_and_wait_for_result(self, payload: Any, num_seconds_timeout tx = self.get_transaction(tx_hash=tx_hash, with_results=True) if tx.is_done(): return tx + else: + logger.info("Transaction not yet done.") return ITransactionOnNetwork() From 74865b2993918747e207da33ec849c683211d1f9 Mon Sep 17 00:00:00 2001 From: Andrei Bancioiu Date: Mon, 7 Feb 2022 00:04:30 +0200 Subject: [PATCH 09/44] Define CLIOutputBuilder, redesign console / outfile dumps. Remove duplication - use cli_shared.send_or_simulate(). --- erdpy/cli_contracts.py | 40 +++++-------------- erdpy/cli_delegation.py | 62 ++++++----------------------- erdpy/cli_output.py | 83 +++++++++++++++++++++++++++++++++++++++ erdpy/cli_shared.py | 35 ++++++++++++++--- erdpy/cli_transactions.py | 31 ++++----------- erdpy/cli_validators.py | 61 ++++++---------------------- erdpy/dns.py | 12 +----- erdpy/interfaces.py | 8 +++- erdpy/transactions.py | 17 +++----- 9 files changed, 166 insertions(+), 183 deletions(-) create mode 100644 erdpy/cli_output.py diff --git a/erdpy/cli_contracts.py b/erdpy/cli_contracts.py index 4c726a11..06af9406 100644 --- a/erdpy/cli_contracts.py +++ b/erdpy/cli_contracts.py @@ -1,7 +1,6 @@ -from collections import OrderedDict import logging import os -from typing import Any, Dict, List +from typing import Any, List from pathlib import Path @@ -10,7 +9,6 @@ from erdpy.contracts import CodeMetadata, SmartContract from erdpy.projects import load_project from erdpy.proxy.core import ElrondProxy -from erdpy.simulation import Simulator from erdpy.transactions import Transaction logger = logging.getLogger("cli.contracts") @@ -213,8 +211,7 @@ def deploy(args: Any): logger.info("Contract address: %s", contract.address) utils.log_explorer_contract_address(chain, contract.address) - output = _send_or_simulate(tx, contract, args) - utils.dump_out_json(output, outfile=args.outfile) + _send_or_simulate(tx, contract, args) def _prepare_contract(args: Any) -> SmartContract: @@ -226,7 +223,7 @@ def _prepare_contract(args: Any) -> SmartContract: bytecode = project.get_bytecode() metadata = CodeMetadata(upgradeable=args.metadata_upgradeable, readable=args.metadata_readable, - payable=args.metadata_payable, payable_by_sc=args.metadata_payable_by_sc) + payable=args.metadata_payable, payable_by_sc=args.metadata_payable_by_sc) contract = SmartContract(bytecode=bytecode, metadata=metadata) return contract @@ -266,8 +263,7 @@ def call(args: Any): sender = _prepare_sender(args) tx = contract.execute(sender, function, arguments, gas_price, gas_limit, value, chain, version) - output = _send_or_simulate(tx, contract, args) - utils.dump_out_json(output, outfile=args.outfile) + _send_or_simulate(tx, contract, args) def upgrade(args: Any): @@ -287,8 +283,7 @@ def upgrade(args: Any): sender = _prepare_sender(args) tx = contract.upgrade(sender, arguments, gas_price, gas_limit, value, chain, version) - output = _send_or_simulate(tx, contract, args) - utils.dump_out_json(output, outfile=args.outfile) + _send_or_simulate(tx, contract, args) def query(args: Any): @@ -303,24 +298,7 @@ def query(args: Any): utils.dump_out_json(result) -def _send_or_simulate(tx: Transaction, contract: SmartContract, args: Any) -> Dict[str, Any]: - proxy = ElrondProxy(args.proxy) - - send_wait_result = args.wait_result and args.send and not args.simulate - send_only = args.send and not (args.wait_result or args.simulate) - simulate = args.simulate and not (send_only or send_wait_result) - - output: OrderedDict[str, Any] = OrderedDict() - - if send_wait_result: - output["txOnNetwork"] = tx.send_wait_result(proxy, args.timeout) - elif send_only: - tx.send(proxy) - elif simulate: - output["txSimulation"] = Simulator(proxy).run(tx) - - # For backwards compatibility (a lot of interaction scripts rely on this attributes): - output["emitted_tx"] = tx.to_dump_dict() - output["emitted_tx"]["address"] = contract.address.bech32() - - return output +def _send_or_simulate(tx: Transaction, contract: SmartContract, args: Any): + output_builder = cli_shared.send_or_simulate(tx, args, dump_output=False) + output_builder.set_contract_address(contract.address) + utils.dump_out_json(output_builder.build(), outfile=args.outfile) diff --git a/erdpy/cli_delegation.py b/erdpy/cli_delegation.py index 6d389841..b7d64cf5 100644 --- a/erdpy/cli_delegation.py +++ b/erdpy/cli_delegation.py @@ -1,5 +1,4 @@ import binascii -import sys from typing import Any, List from erdpy import cli_shared, errors, utils @@ -146,10 +145,7 @@ def do_create_delegation_contract(args: Any): staking_provider.prepare_args_for_create_new_staking_contract(args) tx = do_prepare_transaction(args) - try: - cli_shared.send_or_simulate(tx, args) - finally: - tx.dump_to(args.outfile) + cli_shared.send_or_simulate(tx, args) def get_contract_address_by_deploy_tx_hash(args: Any): @@ -169,10 +165,7 @@ def add_new_nodes(args: Any): staking_provider.prepare_args_for_add_nodes(args) tx = do_prepare_transaction(args) - try: - cli_shared.send_or_simulate(tx, args) - finally: - tx.dump_to(args.outfile) + cli_shared.send_or_simulate(tx, args) def remove_nodes(args: Any): @@ -181,10 +174,7 @@ def remove_nodes(args: Any): staking_provider.prepare_args_for_remove_nodes(args) tx = do_prepare_transaction(args) - try: - cli_shared.send_or_simulate(tx, args) - finally: - tx.dump_to(args.outfile) + cli_shared.send_or_simulate(tx, args) def stake_nodes(args: Any): @@ -193,10 +183,7 @@ def stake_nodes(args: Any): staking_provider.prepare_args_for_stake_nodes(args) tx = do_prepare_transaction(args) - try: - cli_shared.send_or_simulate(tx, args) - finally: - tx.dump_to(args.outfile) + cli_shared.send_or_simulate(tx, args) def unbond_nodes(args: Any): @@ -205,10 +192,7 @@ def unbond_nodes(args: Any): staking_provider.prepare_args_for_unbond_nodes(args) tx = do_prepare_transaction(args) - try: - cli_shared.send_or_simulate(tx, args) - finally: - tx.dump_to(args.outfile) + cli_shared.send_or_simulate(tx, args) def unstake_nodes(args: Any): @@ -217,10 +201,7 @@ def unstake_nodes(args: Any): staking_provider.prepare_args_for_unstake_nodes(args) tx = do_prepare_transaction(args) - try: - cli_shared.send_or_simulate(tx, args) - finally: - tx.dump_to(args.outfile) + cli_shared.send_or_simulate(tx, args) def unjail_nodes(args: Any): @@ -229,10 +210,7 @@ def unjail_nodes(args: Any): staking_provider.prepare_args_for_unjail_nodes(args) tx = do_prepare_transaction(args) - try: - cli_shared.send_or_simulate(tx, args) - finally: - tx.dump_to(args.outfile) + cli_shared.send_or_simulate(tx, args) def change_service_fee(args: Any): @@ -241,11 +219,7 @@ def change_service_fee(args: Any): staking_provider.prepare_args_change_service_fee(args) tx = do_prepare_transaction(args) - try: - cli_shared.send_or_simulate(tx, args) - finally: - tx.dump_to(args.outfile) - + cli_shared.send_or_simulate(tx, args) def modify_delegation_cap(args: Any): cli_shared.check_broadcast_args(args) @@ -253,10 +227,7 @@ def modify_delegation_cap(args: Any): staking_provider.prepare_args_modify_delegation_cap(args) tx = do_prepare_transaction(args) - try: - cli_shared.send_or_simulate(tx, args) - finally: - tx.dump_to(args.outfile) + cli_shared.send_or_simulate(tx, args) def automatic_activation(args: Any): @@ -265,10 +236,7 @@ def automatic_activation(args: Any): staking_provider.prepare_args_automatic_activation(args) tx = do_prepare_transaction(args) - try: - cli_shared.send_or_simulate(tx, args) - finally: - tx.dump_to(args.outfile) + cli_shared.send_or_simulate(tx, args) def redelegate_cap(args: Any): @@ -277,10 +245,7 @@ def redelegate_cap(args: Any): staking_provider.prepare_args_redelegate_cap(args) tx = do_prepare_transaction(args) - try: - cli_shared.send_or_simulate(tx, args) - finally: - tx.dump_to(args.outfile) + cli_shared.send_or_simulate(tx, args) def set_metadata(args: Any): @@ -289,10 +254,7 @@ def set_metadata(args: Any): staking_provider.prepare_args_set_metadata(args) tx = do_prepare_transaction(args) - try: - cli_shared.send_or_simulate(tx, args) - finally: - tx.dump_to(args.outfile) + cli_shared.send_or_simulate(tx, args) def _get_sc_address_from_tx(data: Any): diff --git a/erdpy/cli_output.py b/erdpy/cli_output.py new file mode 100644 index 00000000..888bba51 --- /dev/null +++ b/erdpy/cli_output.py @@ -0,0 +1,83 @@ +from collections import OrderedDict +from typing import Any, Dict, List, Union +from erdpy import utils + +from erdpy.accounts import Address +from erdpy.interfaces import ITransaction +from erdpy.utils import ISerializable + + +class CLIOutputBuilder: + def __init__(self) -> None: + self.emitted_transaction: Union[ITransaction, None] = None + self.emitted_transaction_omitted_fields: List[str] = [] + self.contract_address: Union[Address, None] = None + self.transaction_on_network: Union[ISerializable, None] = None + self.transaction_on_network_omitted_fields: List[str] = [] + self.simulation_results: Union[ISerializable, None] = None + + def set_emitted_transaction(self, emitted_transaction: ITransaction, omitted_fields: List[str] = []): + self.emitted_transaction = emitted_transaction + self.emitted_transaction_omitted_fields = omitted_fields + return self + + def set_contract_address(self, contract_address: Address): + self.contract_address = contract_address + return self + + def set_transaction_on_network(self, transaction_on_network: ISerializable, omitted_fields: List[str] = []): + self.transaction_on_network = transaction_on_network + self.transaction_on_network_omitted_fields = omitted_fields + return self + + def set_simulation_results(self, simulation_results: ISerializable): + self.simulation_results = simulation_results + return self + + # TODO: Remove redundant fields in future versions of erdpy. + # Currently, the following deprecated fields are kept for backwards compatibility: + # tx, hash, data, emitted_tx, emitted_tx.* + def build(self) -> Dict[str, Any]: + output: Dict[str, Any] = OrderedDict() + + if self.emitted_transaction: + emitted_transaction_dict = self.emitted_transaction.to_dictionary() + emitted_transaction_hash = self.emitted_transaction.get_hash() or "" + emitted_transaction_data = self.emitted_transaction.get_data() or "" + utils.omit_fields(emitted_transaction_dict, self.emitted_transaction_omitted_fields) + + output["emittedTransaction"] = emitted_transaction_dict + output["emittedTransactionData"] = emitted_transaction_data + output["emittedTransactionHash"] = emitted_transaction_hash + + # For backwards compatibility (some scripts might rely on these fields): + output["tx"] = emitted_transaction_dict + output["data"] = emitted_transaction_data + output["hash"] = emitted_transaction_hash + + # For backwards compatibility (a lot of interaction scripts rely on "emitted_tx"): + output["emitted_tx"] = { + "tx": emitted_transaction_dict, + "hash": emitted_transaction_hash, + "data": emitted_transaction_data + } + + if self.contract_address: + contract_address = self.contract_address.bech32() + + output["contractAddress"] = contract_address + + # For backwards compatibility (a lot of interaction scripts rely on "emitted_tx"): + if "emitted_tx" in output: + output["emitted_tx"]["address"] = contract_address + + if self.transaction_on_network: + transaction_on_network_dict = self.transaction_on_network.to_dictionary() + utils.omit_fields(transaction_on_network_dict, self.transaction_on_network_omitted_fields) + + output["transactionOnNetwork"] = transaction_on_network_dict + + if self.simulation_results: + output["simulation"] = self.simulation_results + + return output diff --git a/erdpy/cli_shared.py b/erdpy/cli_shared.py index e7afa121..04d840aa 100644 --- a/erdpy/cli_shared.py +++ b/erdpy/cli_shared.py @@ -6,6 +6,7 @@ from erdpy import config, errors, scope, utils from erdpy.accounts import Account +from erdpy.cli_output import CLIOutputBuilder from erdpy.ledger.ledger_functions import do_get_ledger_address from erdpy.proxy.core import ElrondProxy from erdpy.simulation import Simulator @@ -141,13 +142,35 @@ def check_broadcast_args(args: Any): raise errors.BadUsage("Cannot both 'simulate' and 'send' a transaction") -def send_or_simulate(tx: Transaction, args: Any): +def send_or_simulate(tx: Transaction, args: Any, dump_output: bool = True) -> CLIOutputBuilder: proxy = ElrondProxy(args.proxy) - if args.send: - tx.send(proxy) - elif args.simulate: - simulation = Simulator(proxy).run(tx) - utils.dump_out_json(simulation) + + is_set_wait_result = hasattr(args, "wait_result") and args.wait_result + is_set_send = hasattr(args, "send") and args.send + is_set_simulate = hasattr(args, "simulate") and args.simulate + + send_wait_result = is_set_wait_result and is_set_send and not is_set_simulate + send_only = is_set_send and not (is_set_wait_result or is_set_simulate) + simulate = is_set_simulate and not (send_only or send_wait_result) + + output_builder = CLIOutputBuilder() + output_builder.set_emitted_transaction(tx) + outfile = args.outfile if hasattr(args, "outfile") else None + + try: + if send_wait_result: + transaction_on_network = tx.send_wait_result(proxy, args.timeout) + output_builder.set_transaction_on_network(transaction_on_network) + elif send_only: + tx.send(proxy) + elif simulate: + simulation = Simulator(proxy).run(tx) + output_builder.set_simulation_results(simulation) + finally: + if dump_output: + utils.dump_out_json(output_builder.build(), outfile=outfile) + + return output_builder def check_if_sign_method_required(args: List[str], checked_method: str) -> bool: diff --git a/erdpy/cli_transactions.py b/erdpy/cli_transactions.py index 6300e7e2..a3aafb0b 100644 --- a/erdpy/cli_transactions.py +++ b/erdpy/cli_transactions.py @@ -1,9 +1,9 @@ from argparse import FileType -from typing import Any, Dict, List +from typing import Any, List from erdpy import cli_shared, utils +from erdpy.cli_output import CLIOutputBuilder from erdpy.proxy.core import ElrondProxy -from erdpy.simulation import Simulator from erdpy.transactions import Transaction, do_prepare_transaction @@ -61,23 +61,7 @@ def create_transaction(args: Any): args.outfile.write(tx.serialize_as_inner()) return - send_wait_result = args.wait_result and args.send and not args.simulate - send_only = args.send and not (args.wait_result or args.simulate) - simulate = args.simulate and not (send_only or send_wait_result) - - proxy = ElrondProxy(args.proxy) - output: Dict[str, Any] = dict() - - try: - if send_wait_result: - output["txOnNetwork"] = tx.send_wait_result(proxy, args.timeout) - elif send_only: - tx.send(proxy) - elif simulate: - output["txSimulation"] = Simulator(proxy).run(tx) - finally: - output.update(tx.to_dump_dict()) - utils.dump_out_json(output, outfile=args.outfile) + cli_shared.send_or_simulate(tx, args) def send_transaction(args: Any): @@ -88,16 +72,15 @@ def send_transaction(args: Any): try: tx.send(ElrondProxy(args.proxy)) finally: - tx.dump_to(args.outfile) + output = CLIOutputBuilder().set_emitted_transaction(tx).build() + utils.dump_out_json(output, outfile=args.outfile) def get_transaction(args: Any): args = utils.as_object(args) omit_fields = cli_shared.parse_omit_fields_arg(args) - proxy = ElrondProxy(args.proxy) transaction = proxy.get_transaction(args.hash, args.sender, args.with_results) - transaction_dictionary = transaction.to_dictionary() - utils.omit_fields(transaction_dictionary, omit_fields) - utils.dump_out_json(transaction_dictionary) + output = CLIOutputBuilder().set_transaction_on_network(transaction, omit_fields).build() + utils.dump_out_json(output) diff --git a/erdpy/cli_validators.py b/erdpy/cli_validators.py index 99dac174..6aa9351c 100644 --- a/erdpy/cli_validators.py +++ b/erdpy/cli_validators.py @@ -1,4 +1,3 @@ -import sys from typing import Any, List from erdpy import cli_shared, validators, utils @@ -104,10 +103,7 @@ def do_stake(args: Any): validators.prepare_args_for_stake(args) tx = do_prepare_transaction(args) - try: - cli_shared.send_or_simulate(tx, args) - finally: - tx.dump_to(args.outfile) + cli_shared.send_or_simulate(tx, args) def do_unstake(args: Any): @@ -116,10 +112,7 @@ def do_unstake(args: Any): validators.prepare_args_for_unstake(args) tx = do_prepare_transaction(args) - try: - cli_shared.send_or_simulate(tx, args) - finally: - tx.dump_to(args.outfile) + cli_shared.send_or_simulate(tx, args) def do_unjail(args: Any): @@ -128,10 +121,7 @@ def do_unjail(args: Any): validators.prepare_args_for_unjail(args) tx = do_prepare_transaction(args) - try: - cli_shared.send_or_simulate(tx, args) - finally: - tx.dump_to(args.outfile) + cli_shared.send_or_simulate(tx, args) def do_unbond(args: Any): @@ -140,10 +130,7 @@ def do_unbond(args: Any): validators.prepare_args_for_unbond(args) tx = do_prepare_transaction(args) - try: - cli_shared.send_or_simulate(tx, args) - finally: - tx.dump_to(args.outfile) + cli_shared.send_or_simulate(tx, args) def change_reward_address(args: Any): @@ -152,10 +139,7 @@ def change_reward_address(args: Any): validators.prepare_args_for_change_reward_address(args) tx = do_prepare_transaction(args) - try: - cli_shared.send_or_simulate(tx, args) - finally: - tx.dump_to(args.outfile) + cli_shared.send_or_simulate(tx, args) def do_claim(args: Any): @@ -164,10 +148,7 @@ def do_claim(args: Any): validators.prepare_args_for_claim(args) tx = do_prepare_transaction(args) - try: - cli_shared.send_or_simulate(tx, args) - finally: - tx.dump_to(args.outfile) + cli_shared.send_or_simulate(tx, args) def do_unstake_nodes(args: Any): @@ -176,10 +157,7 @@ def do_unstake_nodes(args: Any): validators.prepare_args_for_unstake_nodes(args) tx = do_prepare_transaction(args) - try: - cli_shared.send_or_simulate(tx, args) - finally: - tx.dump_to(args.outfile) + cli_shared.send_or_simulate(tx, args) def do_unstake_tokens(args: Any): @@ -188,10 +166,7 @@ def do_unstake_tokens(args: Any): validators.prepare_args_for_unstake_tokens(args) tx = do_prepare_transaction(args) - try: - cli_shared.send_or_simulate(tx, args) - finally: - tx.dump_to(args.outfile) + cli_shared.send_or_simulate(tx, args) def do_unbond_nodes(args: Any): @@ -200,10 +175,7 @@ def do_unbond_nodes(args: Any): validators.prepare_args_for_unbond_nodes(args) tx = do_prepare_transaction(args) - try: - cli_shared.send_or_simulate(tx, args) - finally: - tx.dump_to(args.outfile) + cli_shared.send_or_simulate(tx, args) def do_unbond_tokens(args: Any): @@ -212,10 +184,7 @@ def do_unbond_tokens(args: Any): validators.prepare_args_for_unbond_tokens(args) tx = do_prepare_transaction(args) - try: - cli_shared.send_or_simulate(tx, args) - finally: - tx.dump_to(args.outfile) + cli_shared.send_or_simulate(tx, args) def do_clean_registered_data(args: Any): @@ -224,10 +193,7 @@ def do_clean_registered_data(args: Any): validators.prepare_args_for_clean_registered_data(args) tx = do_prepare_transaction(args) - try: - cli_shared.send_or_simulate(tx, args) - finally: - tx.dump_to(args.outfile) + cli_shared.send_or_simulate(tx, args) def do_restake_unstaked_nodes(args: Any): @@ -236,7 +202,4 @@ def do_restake_unstaked_nodes(args: Any): validators.prepare_args_for_restake_unstaked_nodes(args) tx = do_prepare_transaction(args) - try: - cli_shared.send_or_simulate(tx, args) - finally: - tx.dump_to(args.outfile) + cli_shared.send_or_simulate(tx, args) diff --git a/erdpy/dns.py b/erdpy/dns.py index e6ec0421..6d87d44a 100644 --- a/erdpy/dns.py +++ b/erdpy/dns.py @@ -6,7 +6,6 @@ from erdpy.accounts import Account, Address from erdpy.contracts import SmartContract from erdpy.proxy.core import ElrondProxy -from erdpy.simulation import Simulator from erdpy.transactions import do_prepare_transaction from erdpy.interfaces import IElrondProxy @@ -46,16 +45,7 @@ def register(args: Any): args.outfile.write(tx.serialize_as_inner()) return - proxy = ElrondProxy(args.proxy) - - try: - if args.send: - tx.send(proxy) - elif args.simulate: - simulation = Simulator(proxy).run(tx) - utils.dump_out_json(simulation) - finally: - tx.dump_to(args.outfile) + cli_shared.send_or_simulate(tx, args) def compute_all_dns_addresses() -> List[Address]: diff --git a/erdpy/interfaces.py b/erdpy/interfaces.py index f6976022..5b09f9ac 100644 --- a/erdpy/interfaces.py +++ b/erdpy/interfaces.py @@ -13,7 +13,7 @@ def pubkey(self) -> bytes: return bytes() -class ITransaction: +class ITransaction(ISerializable): def serialize(self) -> bytes: return bytes() @@ -32,6 +32,12 @@ def set_version(self, version: int): def set_options(self, options: int): return + def get_hash(self) -> str: + return "" + + def get_data(self) -> str: + return "" + class IAccount: def sign_transaction(self, transaction: ITransaction) -> str: diff --git a/erdpy/transactions.py b/erdpy/transactions.py index 0b80d98e..97a6a1b6 100644 --- a/erdpy/transactions.py +++ b/erdpy/transactions.py @@ -90,17 +90,6 @@ def load_from_file(cls, f: TextIO): instance.receiverUsername = instance.receiver_username_encoded() return instance - def to_dump_dict(self): - dump_dict: Dict[str, Any] = dict() - dump_dict['tx'] = self.to_dictionary() - dump_dict['hash'] = self.hash or "" - dump_dict['data'] = self.data - return dump_dict - - def dump_to(self, f: Any): - dump_dict: Any = self.to_dump_dict() - utils.dump_out_json(dump_dict, f) - def send(self, proxy: IElrondProxy): if not self.signature: raise errors.TransactionIsNotSigned() @@ -173,6 +162,12 @@ def set_version(self, version: int): def set_options(self, options: int): self.options = options + def get_data(self) -> str: + return self.data + + def get_hash(self) -> str: + return self.hash + class BunchOfTransactions: def __init__(self): From f1b80096ac7a11aa8b275ac4a00af1bf921b936c Mon Sep 17 00:00:00 2001 From: Andrei Bancioiu Date: Mon, 7 Feb 2022 10:02:19 +0200 Subject: [PATCH 10/44] Parse return messages / output arguments from SCR. --- erdpy/proxy/messages.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/erdpy/proxy/messages.py b/erdpy/proxy/messages.py index 82c54819..365ddb14 100644 --- a/erdpy/proxy/messages.py +++ b/erdpy/proxy/messages.py @@ -53,14 +53,36 @@ def to_dictionary(self) -> Dict[str, Any]: class SmartContractResult(ISerializable): def __init__(self, raw: Dict[str, Any]) -> None: self.raw = raw - self.parsed_data = decode_hex_base64(raw.get("data")) + self.return_message = self._parse_return_message() + self.arguments = self._parse_arguments() self.parsed_log = Log(raw.get("logs", {})) + def _parse_return_message(self) -> str: + try: + data_parts = self._parse_data_parts() + return_message_encoded = data_parts[0] + return_message = bytes.fromhex(return_message_encoded).decode("ascii") + return return_message + except: + return "" + + def _parse_arguments(self) -> List[str]: + try: + data_parts = self._parse_data_parts() + arguments = data_parts[1:] + return arguments + except: + return [] + + def _parse_data_parts(self) -> List[str]: + return self.raw.get("data", "").split("@") + def to_dictionary(self) -> Dict[str, Any]: result: Dict[str, Any] = dict() result.update(self.raw) result["parsed"] = { - "data": self.parsed_data, + "returnMessage": self.return_message, + "arguments": self.arguments, "log": self.parsed_log } @@ -112,4 +134,3 @@ def decode_hex_base64(input: Union[str, None]) -> str: def decode_base64(input: Union[str, None]) -> str: return base64.b64decode(input).decode() if input else "" - From b36230e66ebae8bc7667a9a84c9d3626d8454a2b Mon Sep 17 00:00:00 2001 From: Andrei Bancioiu Date: Mon, 7 Feb 2022 10:03:27 +0200 Subject: [PATCH 11/44] send_transaction_and_wait_for_result - optimization, refactor. --- erdpy/proxy/core.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erdpy/proxy/core.py b/erdpy/proxy/core.py index e71859e9..a36c6bc0 100644 --- a/erdpy/proxy/core.py +++ b/erdpy/proxy/core.py @@ -9,6 +9,7 @@ METACHAIN_ID = 4294967295 ANY_SHARD_ID = 0 +AWAIT_TRANSACTION_PERIOD = 5 logger = logging.getLogger("proxy") @@ -148,7 +149,7 @@ def send_transaction_and_wait_for_result(self, payload: Any, num_seconds_timeout tx_hash = response.get("txHash") for _ in range(0, num_seconds_timeout): - time.sleep(1) + time.sleep(AWAIT_TRANSACTION_PERIOD) tx = self.get_transaction(tx_hash=tx_hash, with_results=True) if tx.is_done(): From f211f9ec6a0c621a05fb088d486e28258565eebe Mon Sep 17 00:00:00 2001 From: Andrei Bancioiu Date: Mon, 7 Feb 2022 10:57:10 +0200 Subject: [PATCH 12/44] Improve output. Add description for CLI output. --- erdpy/cli_contracts.py | 8 +++-- erdpy/cli_output.py | 68 +++++++++++++++++++++++++++++---------- erdpy/cli_shared.py | 4 +-- erdpy/cli_transactions.py | 8 ++--- erdpy/proxy/messages.py | 27 +++++++++------- 5 files changed, 77 insertions(+), 38 deletions(-) diff --git a/erdpy/cli_contracts.py b/erdpy/cli_contracts.py index 06af9406..9069b646 100644 --- a/erdpy/cli_contracts.py +++ b/erdpy/cli_contracts.py @@ -6,6 +6,7 @@ from erdpy import cli_shared, errors, projects, utils from erdpy.accounts import Account, Address, LedgerAccount +from erdpy.cli_output import CLIOutputBuilder from erdpy.contracts import CodeMetadata, SmartContract from erdpy.projects import load_project from erdpy.proxy.core import ElrondProxy @@ -56,7 +57,8 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: sub.add_argument("--wildcard", required=False, help="wildcard to match only specific test files") sub.set_defaults(func=run_tests) - sub = cli_shared.add_command_subparser(subparsers, "contract", "deploy", "Deploy a Smart Contract.") + output_description = CLIOutputBuilder.describe(with_contract=True, with_awaited_transaction=True, with_simulation=True) + sub = cli_shared.add_command_subparser(subparsers, "contract", "deploy", f"Deploy a Smart Contract.{output_description}") _add_project_or_bytecode_arg(sub) _add_metadata_arg(sub) cli_shared.add_outfile_arg(sub) @@ -73,7 +75,7 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: sub.set_defaults(func=deploy) sub = cli_shared.add_command_subparser(subparsers, "contract", "call", - "Interact with a Smart Contract (execute function).") + f"Interact with a Smart Contract (execute function).{output_description}") _add_contract_arg(sub) cli_shared.add_outfile_arg(sub) cli_shared.add_wallet_args(args, sub) @@ -90,7 +92,7 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: sub.set_defaults(func=call) sub = cli_shared.add_command_subparser(subparsers, "contract", "upgrade", - "Upgrade a previously-deployed Smart Contract") + "Upgrade a previously-deployed Smart Contract.{output_description}") _add_contract_arg(sub) cli_shared.add_outfile_arg(sub) _add_project_or_bytecode_arg(sub) diff --git a/erdpy/cli_output.py b/erdpy/cli_output.py index 888bba51..8360e2ba 100644 --- a/erdpy/cli_output.py +++ b/erdpy/cli_output.py @@ -1,19 +1,23 @@ from collections import OrderedDict +import json from typing import Any, Dict, List, Union +import logging from erdpy import utils from erdpy.accounts import Address from erdpy.interfaces import ITransaction from erdpy.utils import ISerializable +logger = logging.getLogger("cli.output") + class CLIOutputBuilder: def __init__(self) -> None: self.emitted_transaction: Union[ITransaction, None] = None self.emitted_transaction_omitted_fields: List[str] = [] self.contract_address: Union[Address, None] = None - self.transaction_on_network: Union[ISerializable, None] = None - self.transaction_on_network_omitted_fields: List[str] = [] + self.awaited_transaction: Union[ISerializable, None] = None + self.awaited_transaction_omitted_fields: List[str] = [] self.simulation_results: Union[ISerializable, None] = None def set_emitted_transaction(self, emitted_transaction: ITransaction, omitted_fields: List[str] = []): @@ -25,18 +29,15 @@ def set_contract_address(self, contract_address: Address): self.contract_address = contract_address return self - def set_transaction_on_network(self, transaction_on_network: ISerializable, omitted_fields: List[str] = []): - self.transaction_on_network = transaction_on_network - self.transaction_on_network_omitted_fields = omitted_fields + def set_awaited_transaction(self, awaited_transaction: ISerializable, omitted_fields: List[str] = []): + self.awaited_transaction = awaited_transaction + self.awaited_transaction_omitted_fields = omitted_fields return self def set_simulation_results(self, simulation_results: ISerializable): self.simulation_results = simulation_results return self - # TODO: Remove redundant fields in future versions of erdpy. - # Currently, the following deprecated fields are kept for backwards compatibility: - # tx, hash, data, emitted_tx, emitted_tx.* def build(self) -> Dict[str, Any]: output: Dict[str, Any] = OrderedDict() @@ -50,12 +51,12 @@ def build(self) -> Dict[str, Any]: output["emittedTransactionData"] = emitted_transaction_data output["emittedTransactionHash"] = emitted_transaction_hash - # For backwards compatibility (some scripts might rely on these fields): + logger.warn("The fields 'tx', 'data', 'hash' are deprecated and will be removed in a future version. Please rely on 'emittedTransaction', 'emittedTransactionData' and 'emittedTransactionHash' instead.") output["tx"] = emitted_transaction_dict output["data"] = emitted_transaction_data output["hash"] = emitted_transaction_hash - # For backwards compatibility (a lot of interaction scripts rely on "emitted_tx"): + logger.warn("The field 'emitted_tx' is deprecated and will be removed in a future version. Please rely on 'emittedTransaction' instead.") output["emitted_tx"] = { "tx": emitted_transaction_dict, "hash": emitted_transaction_hash, @@ -64,20 +65,53 @@ def build(self) -> Dict[str, Any]: if self.contract_address: contract_address = self.contract_address.bech32() - output["contractAddress"] = contract_address - # For backwards compatibility (a lot of interaction scripts rely on "emitted_tx"): + logger.warn("The field 'emitted_tx.address' is deprecated and will be removed in a future version. Please rely on 'contractAddress' instead.") if "emitted_tx" in output: output["emitted_tx"]["address"] = contract_address - if self.transaction_on_network: - transaction_on_network_dict = self.transaction_on_network.to_dictionary() - utils.omit_fields(transaction_on_network_dict, self.transaction_on_network_omitted_fields) - - output["transactionOnNetwork"] = transaction_on_network_dict + if self.awaited_transaction: + awaited_transaction_dict = self.awaited_transaction.to_dictionary() + utils.omit_fields(awaited_transaction_dict, self.awaited_transaction_omitted_fields) + output["awaitedTransaction"] = awaited_transaction_dict if self.simulation_results: output["simulation"] = self.simulation_results return output + + @classmethod + def describe(cls, with_emitted: bool = True, with_contract: bool = False, with_awaited_transaction: bool = False, with_simulation: bool = False) -> str: + output: Dict[str, Any] = OrderedDict() + + if with_emitted: + output["emittedTransaction"] = {"nonce": 42, "sender": "alice", "receiver": "bob", "...": "..."} + output["emittedTransactionData"] = "the transaction data, not encoded" + output["emittedTransactionHash"] = "the transaction hash" + + output["tx"] = {"DEPRECATED": "DEPRECATED"} + output["data"] = "DEPRECATED" + output["hash"] = "DEPRECATED" + + if with_contract: + output["emitted_tx"] = {"DEPRECATED": "DEPRECATED"} + output["contractAddress"] = "the address of the contract (in case of deployments)" + + if with_awaited_transaction: + output["awaitedTransaction"] = {"nonce": 42, "sender": "alice", "receiver": "bob", "...": "..."} + + if with_simulation: + output["simulation"] = { + "execution": {"...": "..."}, + "cost": {"...": "..."} + } + + description = json.dumps(output, indent=4) + description_wrapped = f""" + +Output example: +=============== +{description} +""" + return description_wrapped diff --git a/erdpy/cli_shared.py b/erdpy/cli_shared.py index 04d840aa..a5af63b9 100644 --- a/erdpy/cli_shared.py +++ b/erdpy/cli_shared.py @@ -14,7 +14,7 @@ def wider_help_formatter(prog: Text): - return argparse.HelpFormatter(prog, max_help_position=50, width=120) + return argparse.RawDescriptionHelpFormatter(prog, max_help_position=50, width=120) def add_group_subparser(subparsers: Any, group: str, description: str) -> Any: @@ -160,7 +160,7 @@ def send_or_simulate(tx: Transaction, args: Any, dump_output: bool = True) -> CL try: if send_wait_result: transaction_on_network = tx.send_wait_result(proxy, args.timeout) - output_builder.set_transaction_on_network(transaction_on_network) + output_builder.set_awaited_transaction(transaction_on_network) elif send_only: tx.send(proxy) elif simulate: diff --git a/erdpy/cli_transactions.py b/erdpy/cli_transactions.py index a3aafb0b..2967c0d0 100644 --- a/erdpy/cli_transactions.py +++ b/erdpy/cli_transactions.py @@ -11,7 +11,7 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: parser = cli_shared.add_group_subparser(subparsers, "tx", "Create and broadcast Transactions") subparsers = parser.add_subparsers() - sub = cli_shared.add_command_subparser(subparsers, "tx", "new", "Create a new transaction") + sub = cli_shared.add_command_subparser(subparsers, "tx", "new", f"Create a new transaction.{CLIOutputBuilder.describe()}") _add_common_arguments(args, sub) cli_shared.add_outfile_arg(sub, what="signed transaction, hash") cli_shared.add_broadcast_args(sub, relay=True) @@ -22,13 +22,13 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: " - only valid if --wait-result is set") sub.set_defaults(func=create_transaction) - sub = cli_shared.add_command_subparser(subparsers, "tx", "send", "Send a previously saved transaction") + sub = cli_shared.add_command_subparser(subparsers, "tx", "send", f"Send a previously saved transaction.{CLIOutputBuilder.describe()}") cli_shared.add_infile_arg(sub, what="a previously saved transaction") cli_shared.add_outfile_arg(sub, what="the hash") cli_shared.add_proxy_arg(sub) sub.set_defaults(func=send_transaction) - sub = cli_shared.add_command_subparser(subparsers, "tx", "get", "Get a transaction") + sub = cli_shared.add_command_subparser(subparsers, "tx", "get", f"Get a transaction.{CLIOutputBuilder.describe(with_emitted=False)}") sub.add_argument("--hash", required=True, help="the hash") sub.add_argument("--sender", required=False, help="the sender address") sub.add_argument("--with-results", action="store_true", help="will also return the results of transaction") @@ -82,5 +82,5 @@ def get_transaction(args: Any): proxy = ElrondProxy(args.proxy) transaction = proxy.get_transaction(args.hash, args.sender, args.with_results) - output = CLIOutputBuilder().set_transaction_on_network(transaction, omit_fields).build() + output = CLIOutputBuilder().set_awaited_transaction(transaction, omit_fields).build() utils.dump_out_json(output) diff --git a/erdpy/proxy/messages.py b/erdpy/proxy/messages.py index 365ddb14..ed708db5 100644 --- a/erdpy/proxy/messages.py +++ b/erdpy/proxy/messages.py @@ -41,11 +41,12 @@ def get_hash(self) -> str: def to_dictionary(self) -> Dict[str, Any]: result: Dict[str, Any] = dict() result.update(self.raw) - result["hash"] = self.hash - result["parsed"] = { - "smartContractResults": self.parsed_contract_results, - "logs": self.parsed_logs - } + result["parsed"] = dict() + + if self.parsed_contract_results: + result["smartContractResults"] = self.parsed_contract_results + if self.parsed_logs: + result["logs"] = self.parsed_logs return result @@ -104,9 +105,10 @@ def __init__(self, response: GenericProxyResponse) -> None: def to_dictionary(self) -> Dict[str, Any]: result: Dict[str, Any] = dict() result.update(self.raw) - result["parsed"] = { - "smartContractResults": self.parsed_contract_results - } + result["parsed"] = dict() + + if self.parsed_contract_results: + result["smartContractResults"] = self.parsed_contract_results return result @@ -120,10 +122,11 @@ def __init__(self, response: GenericProxyResponse) -> None: def to_dictionary(self) -> Dict[str, Any]: result: Dict[str, Any] = dict() - result.update(self.raw) - result["parsed"] = { - "smartContractResults": self.parsed_contract_results - } + result.update(self.raw) + result["parsed"] = dict() + + if self.parsed_contract_results: + result["smartContractResults"] = self.parsed_contract_results return result From ba358dd6c1aa970e30de8e73119e96630e33b8d5 Mon Sep 17 00:00:00 2001 From: Andrei Bancioiu Date: Mon, 7 Feb 2022 11:14:49 +0200 Subject: [PATCH 13/44] Fix output & description. --- erdpy/CLI.md | 174 +++++++++++++++++++++++++++++++++++--- erdpy/cli_contracts.py | 4 +- erdpy/cli_output.py | 27 +++--- erdpy/cli_shared.py | 3 +- erdpy/cli_transactions.py | 4 +- 5 files changed, 185 insertions(+), 27 deletions(-) diff --git a/erdpy/CLI.md b/erdpy/CLI.md index 755e37d4..9d4eb6bf 100644 --- a/erdpy/CLI.md +++ b/erdpy/CLI.md @@ -73,7 +73,7 @@ clean Clean a Smart Contract project. test Run Mandos tests. deploy Deploy a Smart Contract. call Interact with a Smart Contract (execute function). -upgrade Upgrade a previously-deployed Smart Contract +upgrade Upgrade a previously-deployed Smart Contract. query Query a Smart Contract (call a pure function) ``` @@ -157,6 +157,42 @@ usage: erdpy contract deploy [-h] ... Deploy a Smart Contract. +Output example: +=============== +{ + "emittedTransaction": { + "nonce": 42, + "sender": "alice", + "receiver": "bob", + "...": "..." + }, + "emittedTransactionData": "the transaction data, not encoded", + "emittedTransactionHash": "the transaction hash", + "tx": { + "DEPRECATED": "DEPRECATED" + }, + "data": "DEPRECATED", + "hash": "DEPRECATED", + "emitted_tx": { + "DEPRECATED": "DEPRECATED" + }, + "contractAddress": "the address of the contract", + "transactionOnNetwork": { + "nonce": 42, + "sender": "alice", + "receiver": "bob", + "...": "..." + }, + "simulation": { + "execution": { + "...": "..." + }, + "cost": { + "...": "..." + } + } +} + optional arguments: -h, --help show this help message and exit --project PROJECT 🗀 the project directory (default: current directory) @@ -202,6 +238,42 @@ usage: erdpy contract call [-h] ... Interact with a Smart Contract (execute function). +Output example: +=============== +{ + "emittedTransaction": { + "nonce": 42, + "sender": "alice", + "receiver": "bob", + "...": "..." + }, + "emittedTransactionData": "the transaction data, not encoded", + "emittedTransactionHash": "the transaction hash", + "tx": { + "DEPRECATED": "DEPRECATED" + }, + "data": "DEPRECATED", + "hash": "DEPRECATED", + "emitted_tx": { + "DEPRECATED": "DEPRECATED" + }, + "contractAddress": "the address of the contract", + "transactionOnNetwork": { + "nonce": 42, + "sender": "alice", + "receiver": "bob", + "...": "..." + }, + "simulation": { + "execution": { + "...": "..." + }, + "cost": { + "...": "..." + } + } +} + positional arguments: contract 🖄 the address of the Smart Contract @@ -244,7 +316,43 @@ optional arguments: $ erdpy contract upgrade --help usage: erdpy contract upgrade [-h] ... -Upgrade a previously-deployed Smart Contract +Upgrade a previously-deployed Smart Contract. + +Output example: +=============== +{ + "emittedTransaction": { + "nonce": 42, + "sender": "alice", + "receiver": "bob", + "...": "..." + }, + "emittedTransactionData": "the transaction data, not encoded", + "emittedTransactionHash": "the transaction hash", + "tx": { + "DEPRECATED": "DEPRECATED" + }, + "data": "DEPRECATED", + "hash": "DEPRECATED", + "emitted_tx": { + "DEPRECATED": "DEPRECATED" + }, + "contractAddress": "the address of the contract", + "transactionOnNetwork": { + "nonce": 42, + "sender": "alice", + "receiver": "bob", + "...": "..." + }, + "simulation": { + "execution": { + "...": "..." + }, + "cost": { + "...": "..." + } + } +} positional arguments: contract 🖄 the address of the Smart Contract @@ -323,9 +431,9 @@ OPTIONS: ---------------- COMMANDS summary ---------------- -new Create a new transaction -send Send a previously saved transaction -get Get a transaction +new Create a new transaction. +send Send a previously saved transaction. +get Get a transaction. ``` ### Transactions.New @@ -335,7 +443,25 @@ get Get a transaction $ erdpy tx new --help usage: erdpy tx new [-h] ... -Create a new transaction +Create a new transaction. + +Output example: +=============== +{ + "emittedTransaction": { + "nonce": 42, + "sender": "alice", + "receiver": "bob", + "...": "..." + }, + "emittedTransactionData": "the transaction data, not encoded", + "emittedTransactionHash": "the transaction hash", + "tx": { + "DEPRECATED": "DEPRECATED" + }, + "data": "DEPRECATED", + "hash": "DEPRECATED" +} optional arguments: -h, --help show this help message and exit @@ -377,7 +503,25 @@ optional arguments: $ erdpy tx send --help usage: erdpy tx send [-h] ... -Send a previously saved transaction +Send a previously saved transaction. + +Output example: +=============== +{ + "emittedTransaction": { + "nonce": 42, + "sender": "alice", + "receiver": "bob", + "...": "..." + }, + "emittedTransactionData": "the transaction data, not encoded", + "emittedTransactionHash": "the transaction hash", + "tx": { + "DEPRECATED": "DEPRECATED" + }, + "data": "DEPRECATED", + "hash": "DEPRECATED" +} optional arguments: -h, --help show this help message and exit @@ -393,7 +537,18 @@ optional arguments: $ erdpy tx get --help usage: erdpy tx get [-h] ... -Get a transaction +Get a transaction. + +Output example: +=============== +{ + "transactionOnNetwork": { + "nonce": 42, + "sender": "alice", + "receiver": "bob", + "...": "..." + } +} optional arguments: -h, --help show this help message and exit @@ -773,8 +928,7 @@ pem-address-hex Get the public address out of a PEM file as hex $ erdpy wallet new --help usage: erdpy wallet new [-h] ... -Create a new wallet and print its mnemonic; optionally save as password-protected JSON (recommended) or PEM (not -recommended) +Create a new wallet and print its mnemonic; optionally save as password-protected JSON (recommended) or PEM (not recommended) optional arguments: -h, --help show this help message and exit diff --git a/erdpy/cli_contracts.py b/erdpy/cli_contracts.py index 9069b646..7fee415a 100644 --- a/erdpy/cli_contracts.py +++ b/erdpy/cli_contracts.py @@ -57,7 +57,7 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: sub.add_argument("--wildcard", required=False, help="wildcard to match only specific test files") sub.set_defaults(func=run_tests) - output_description = CLIOutputBuilder.describe(with_contract=True, with_awaited_transaction=True, with_simulation=True) + output_description = CLIOutputBuilder.describe(with_contract=True, with_transaction_on_network=True, with_simulation=True) sub = cli_shared.add_command_subparser(subparsers, "contract", "deploy", f"Deploy a Smart Contract.{output_description}") _add_project_or_bytecode_arg(sub) _add_metadata_arg(sub) @@ -92,7 +92,7 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: sub.set_defaults(func=call) sub = cli_shared.add_command_subparser(subparsers, "contract", "upgrade", - "Upgrade a previously-deployed Smart Contract.{output_description}") + f"Upgrade a previously-deployed Smart Contract.{output_description}") _add_contract_arg(sub) cli_shared.add_outfile_arg(sub) _add_project_or_bytecode_arg(sub) diff --git a/erdpy/cli_output.py b/erdpy/cli_output.py index 8360e2ba..b00c4090 100644 --- a/erdpy/cli_output.py +++ b/erdpy/cli_output.py @@ -16,8 +16,8 @@ def __init__(self) -> None: self.emitted_transaction: Union[ITransaction, None] = None self.emitted_transaction_omitted_fields: List[str] = [] self.contract_address: Union[Address, None] = None - self.awaited_transaction: Union[ISerializable, None] = None - self.awaited_transaction_omitted_fields: List[str] = [] + self.transaction_on_network: Union[ISerializable, None] = None + self.transaction_on_network_omitted_fields: List[str] = [] self.simulation_results: Union[ISerializable, None] = None def set_emitted_transaction(self, emitted_transaction: ITransaction, omitted_fields: List[str] = []): @@ -30,8 +30,11 @@ def set_contract_address(self, contract_address: Address): return self def set_awaited_transaction(self, awaited_transaction: ISerializable, omitted_fields: List[str] = []): - self.awaited_transaction = awaited_transaction - self.awaited_transaction_omitted_fields = omitted_fields + return self.set_transaction_on_network(awaited_transaction, omitted_fields) + + def set_transaction_on_network(self, transaction_on_network: ISerializable, omitted_fields: List[str] = []): + self.transaction_on_network = transaction_on_network + self.transaction_on_network_omitted_fields = omitted_fields return self def set_simulation_results(self, simulation_results: ISerializable): @@ -71,10 +74,10 @@ def build(self) -> Dict[str, Any]: if "emitted_tx" in output: output["emitted_tx"]["address"] = contract_address - if self.awaited_transaction: - awaited_transaction_dict = self.awaited_transaction.to_dictionary() - utils.omit_fields(awaited_transaction_dict, self.awaited_transaction_omitted_fields) - output["awaitedTransaction"] = awaited_transaction_dict + if self.transaction_on_network: + transaction_on_network_dict = self.transaction_on_network.to_dictionary() + utils.omit_fields(transaction_on_network_dict, self.transaction_on_network_omitted_fields) + output["transactionOnNetwork"] = transaction_on_network_dict if self.simulation_results: output["simulation"] = self.simulation_results @@ -82,7 +85,7 @@ def build(self) -> Dict[str, Any]: return output @classmethod - def describe(cls, with_emitted: bool = True, with_contract: bool = False, with_awaited_transaction: bool = False, with_simulation: bool = False) -> str: + def describe(cls, with_emitted: bool = True, with_contract: bool = False, with_transaction_on_network: bool = False, with_simulation: bool = False) -> str: output: Dict[str, Any] = OrderedDict() if with_emitted: @@ -96,10 +99,10 @@ def describe(cls, with_emitted: bool = True, with_contract: bool = False, with_a if with_contract: output["emitted_tx"] = {"DEPRECATED": "DEPRECATED"} - output["contractAddress"] = "the address of the contract (in case of deployments)" + output["contractAddress"] = "the address of the contract" - if with_awaited_transaction: - output["awaitedTransaction"] = {"nonce": 42, "sender": "alice", "receiver": "bob", "...": "..."} + if with_transaction_on_network: + output["transactionOnNetwork"] = {"nonce": 42, "sender": "alice", "receiver": "bob", "...": "..."} if with_simulation: output["simulation"] = { diff --git a/erdpy/cli_shared.py b/erdpy/cli_shared.py index a5af63b9..165544f3 100644 --- a/erdpy/cli_shared.py +++ b/erdpy/cli_shared.py @@ -37,7 +37,8 @@ def build_group_epilog(subparsers: Any) -> str: ---------------- """ for choice, sub in subparsers.choices.items(): - epilog += f"{choice.ljust(30)} {sub.description}\n" + description_first_line = sub.description.splitlines()[0] + epilog += f"{choice.ljust(30)} {description_first_line}\n" return epilog diff --git a/erdpy/cli_transactions.py b/erdpy/cli_transactions.py index 2967c0d0..51162a6b 100644 --- a/erdpy/cli_transactions.py +++ b/erdpy/cli_transactions.py @@ -28,7 +28,7 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: cli_shared.add_proxy_arg(sub) sub.set_defaults(func=send_transaction) - sub = cli_shared.add_command_subparser(subparsers, "tx", "get", f"Get a transaction.{CLIOutputBuilder.describe(with_emitted=False)}") + sub = cli_shared.add_command_subparser(subparsers, "tx", "get", f"Get a transaction.{CLIOutputBuilder.describe(with_emitted=False, with_transaction_on_network=True)}") sub.add_argument("--hash", required=True, help="the hash") sub.add_argument("--sender", required=False, help="the sender address") sub.add_argument("--with-results", action="store_true", help="will also return the results of transaction") @@ -82,5 +82,5 @@ def get_transaction(args: Any): proxy = ElrondProxy(args.proxy) transaction = proxy.get_transaction(args.hash, args.sender, args.with_results) - output = CLIOutputBuilder().set_awaited_transaction(transaction, omit_fields).build() + output = CLIOutputBuilder().set_transaction_on_network(transaction, omit_fields).build() utils.dump_out_json(output) From d072e0ac86c28a95aadaa73219156eff97202afa Mon Sep 17 00:00:00 2001 From: Andrei Bancioiu Date: Mon, 7 Feb 2022 11:16:58 +0200 Subject: [PATCH 14/44] Fix call to parse_validator_pem() - pass a Path object. --- erdpy/validators/core.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erdpy/validators/core.py b/erdpy/validators/core.py index 8e9ce178..073268cc 100644 --- a/erdpy/validators/core.py +++ b/erdpy/validators/core.py @@ -1,6 +1,7 @@ import binascii import logging from os import path +from pathlib import Path from typing import Any from erdpy.validators.validators_file import ValidatorsFile @@ -37,7 +38,7 @@ def prepare_args_for_stake(args: Any): # get validator validator_pem = validator.get("pemFile") validator_pem = path.join(path.dirname(args.validators_file), validator_pem) - secret_key_bytes, bls_key = parse_validator_pem(validator_pem) + secret_key_bytes, bls_key = parse_validator_pem(Path(validator_pem)) signed_message = sign_message_with_bls_key(account.address.pubkey().hex(), secret_key_bytes.decode('ascii')) stake_data += f"@{bls_key}@{signed_message}" From 95b55f55d8f0db570e3abde7bd71368dbd317c3e Mon Sep 17 00:00:00 2001 From: Andrei Bancioiu Date: Mon, 7 Feb 2022 11:50:11 +0200 Subject: [PATCH 15/44] Fix mypy warnings. --- erdpy/proxy/messages.py | 10 ++++++---- erdpy/transactions.py | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/erdpy/proxy/messages.py b/erdpy/proxy/messages.py index ed708db5..31527f4e 100644 --- a/erdpy/proxy/messages.py +++ b/erdpy/proxy/messages.py @@ -33,7 +33,8 @@ def __init__(self, hash: str, response: GenericProxyResponse) -> None: self.parsed_logs = [Log(item) for item in logs] def is_done(self) -> bool: - return self.raw.get("hyperblockNonce", 0) > 0 + hyperblock: int = self.raw.get("hyperblockNonce", 0) + return hyperblock > 0 def get_hash(self) -> str: return self.hash @@ -76,7 +77,8 @@ def _parse_arguments(self) -> List[str]: return [] def _parse_data_parts(self) -> List[str]: - return self.raw.get("data", "").split("@") + data: str = self.raw.get("data", "") + return data.split("@") def to_dictionary(self) -> Dict[str, Any]: result: Dict[str, Any] = dict() @@ -92,7 +94,7 @@ def to_dictionary(self) -> Dict[str, Any]: class Log(ISerializable): def __init__(self, raw: Dict[str, Any]) -> None: - self.events = [] + self.raw = raw class SimulateResponse(ISimulateResponse): @@ -122,7 +124,7 @@ def __init__(self, response: GenericProxyResponse) -> None: def to_dictionary(self) -> Dict[str, Any]: result: Dict[str, Any] = dict() - result.update(self.raw) + result.update(self.raw) result["parsed"] = dict() if self.parsed_contract_results: diff --git a/erdpy/transactions.py b/erdpy/transactions.py index 97a6a1b6..1294c8ef 100644 --- a/erdpy/transactions.py +++ b/erdpy/transactions.py @@ -13,7 +13,7 @@ class Transaction(ITransaction): def __init__(self): - self.hash = "" + self.hash: str = "" self.nonce = 0 self.value = "0" self.receiver = "" @@ -22,7 +22,7 @@ def __init__(self): self.receiverUsername = "" self.gasPrice = 0 self.gasLimit = 0 - self.data = "" + self.data: str = "" self.chainID = "" self.version = 0 self.options = 0 From b72ed0ea994b02813e95456331c2b4205be25392 Mon Sep 17 00:00:00 2001 From: Andrei Bancioiu Date: Mon, 7 Feb 2022 12:05:29 +0200 Subject: [PATCH 16/44] Partially fix simulation output. --- erdpy/proxy/messages.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/erdpy/proxy/messages.py b/erdpy/proxy/messages.py index 31527f4e..72dedd8b 100644 --- a/erdpy/proxy/messages.py +++ b/erdpy/proxy/messages.py @@ -25,12 +25,11 @@ class TransactionOnNetwork(ITransactionOnNetwork): def __init__(self, hash: str, response: GenericProxyResponse) -> None: raw = response.get("transaction", dict()) contract_results: List[Dict[str, Any]] = raw.get("smartContractResults", []) - logs: List[Dict[str, Any]] = raw.get("logs", []) self.raw = raw self.hash = hash self.parsed_contract_results = [SmartContractResult(item) for item in contract_results] - self.parsed_logs = [Log(item) for item in logs] + self.parsed_logs: List[Log] = [] def is_done(self) -> bool: hyperblock: int = self.raw.get("hyperblockNonce", 0) @@ -45,9 +44,9 @@ def to_dictionary(self) -> Dict[str, Any]: result["parsed"] = dict() if self.parsed_contract_results: - result["smartContractResults"] = self.parsed_contract_results + result["parsed"]["smartContractResults"] = self.parsed_contract_results if self.parsed_logs: - result["logs"] = self.parsed_logs + result["parsed"]["logs"] = self.parsed_logs return result @@ -77,7 +76,7 @@ def _parse_arguments(self) -> List[str]: return [] def _parse_data_parts(self) -> List[str]: - data: str = self.raw.get("data", "") + data: str = self.raw.get("data", "").lstrip("@") return data.split("@") def to_dictionary(self) -> Dict[str, Any]: @@ -99,7 +98,8 @@ def __init__(self, raw: Dict[str, Any]) -> None: class SimulateResponse(ISimulateResponse): def __init__(self, response: GenericProxyResponse) -> None: - contract_results: Dict[str, Any] = response.get("scResults") or dict() + result: Dict[str, Any] = response.get("result") or dict() + contract_results: Dict[str, Any] = result.get("scResults") or dict() self.raw = response.to_dictionary() self.parsed_contract_results = [SmartContractResult(item) for item in contract_results.values()] @@ -110,7 +110,7 @@ def to_dictionary(self) -> Dict[str, Any]: result["parsed"] = dict() if self.parsed_contract_results: - result["smartContractResults"] = self.parsed_contract_results + result["parsed"]["smartContractResults"] = self.parsed_contract_results return result @@ -128,7 +128,7 @@ def to_dictionary(self) -> Dict[str, Any]: result["parsed"] = dict() if self.parsed_contract_results: - result["smartContractResults"] = self.parsed_contract_results + result["parsed"]["smartContractResults"] = self.parsed_contract_results return result From 8696f303b3e664a77c84d6392a643c292121a155 Mon Sep 17 00:00:00 2001 From: Andrei Bancioiu Date: Tue, 8 Feb 2022 22:18:41 +0200 Subject: [PATCH 17/44] Fix after review, plus use gasScheduleV5 on local-testnet. --- erdpy/proxy/core.py | 3 ++- erdpy/testnet/node_config_toml.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erdpy/proxy/core.py b/erdpy/proxy/core.py index a36c6bc0..1c802f59 100644 --- a/erdpy/proxy/core.py +++ b/erdpy/proxy/core.py @@ -147,8 +147,9 @@ def send_transaction_and_wait_for_result(self, payload: Any, num_seconds_timeout url = f"{self.url}/transaction/send" response = do_post(url, payload) tx_hash = response.get("txHash") + num_periods_to_wait = int(num_seconds_timeout / AWAIT_TRANSACTION_PERIOD) - for _ in range(0, num_seconds_timeout): + for _ in range(0, num_periods_to_wait): time.sleep(AWAIT_TRANSACTION_PERIOD) tx = self.get_transaction(tx_hash=tx_hash, with_results=True) diff --git a/erdpy/testnet/node_config_toml.py b/erdpy/testnet/node_config_toml.py index dd473650..f6ab9b10 100644 --- a/erdpy/testnet/node_config_toml.py +++ b/erdpy/testnet/node_config_toml.py @@ -109,7 +109,7 @@ def patch_enable_epochs(data: ConfigDict, testnet_config: TestnetConfiguration): gas_schedule = dict() gas_schedule['GasScheduleByEpochs'] = [ - {'StartEpoch': 0, 'FileName': 'gasScheduleV3.toml'} + {'StartEpoch': 0, 'FileName': 'gasScheduleV5.toml'} ] if validate: From 7016ef81aa3f22493589399808120e00e1682037 Mon Sep 17 00:00:00 2001 From: Andrei Bancioiu Date: Tue, 8 Feb 2022 22:50:30 +0200 Subject: [PATCH 18/44] Local testnet: minor refactoring & remove "*:TRACE" from the default log-level. --- erdpy/testnet/config.py | 2 +- erdpy/testnet/core.py | 33 ++++++++++++++++++++------------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/erdpy/testnet/config.py b/erdpy/testnet/config.py index 07c2e0bd..3535b448 100644 --- a/erdpy/testnet/config.py +++ b/erdpy/testnet/config.py @@ -90,7 +90,7 @@ def __init__(self, config): self.timing.update(self.config.get('timing', dict())) @classmethod - def from_file(cls, filename): + def from_file(cls, filename: str): """ If no filename is specified, try to load testnet.toml from the current directory, if there is any, and merge it with the SDK-level testnet diff --git a/erdpy/testnet/core.py b/erdpy/testnet/core.py index d276f7fa..effdf5f4 100644 --- a/erdpy/testnet/core.py +++ b/erdpy/testnet/core.py @@ -1,7 +1,8 @@ import asyncio import logging +from pathlib import Path import traceback -from typing import Any +from typing import Any, Coroutine, List from erdpy.testnet.config import TestnetConfiguration @@ -28,12 +29,13 @@ async def do_start(args: Any): testnet_config = TestnetConfiguration.from_file(args.configfile) logger.info('testnet folder is %s', testnet_config.root()) - to_run = [] + to_run: List[Coroutine[Any, Any, None]] = [] # Seed node to_run.append(run(["./seednode", "--log-save"], cwd=testnet_config.seednode_folder())) loglevel = _patch_loglevel(testnet_config.loglevel()) + logger.info(f"loglevel: {loglevel}") # Observers for observer in testnet_config.observers(): @@ -69,10 +71,10 @@ async def do_start(args: Any): await asyncio.gather(*to_run) -async def run(args, env=None, cwd: str = None, delay: int = 0): +async def run(args: List[str], cwd: Path, delay: int = 0): await asyncio.sleep(delay) - process = await asyncio.create_subprocess_exec(*args, env=env, stdout=asyncio.subprocess.PIPE, + process = await asyncio.create_subprocess_exec(*args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, cwd=cwd, limit=1024 * 512) pid = process.pid @@ -87,7 +89,7 @@ async def run(args, env=None, cwd: str = None, delay: int = 0): print(f"Proces [{pid}] stopped. Return code: {return_code}.") -async def _read_stream(stream, pid): +async def _read_stream(stream: Any, pid: int): while True: try: line = await stream.readline() @@ -103,26 +105,31 @@ async def _read_stream(stream, pid): def _patch_loglevel(loglevel: str) -> str: loglevel = loglevel or "*:DEBUG" + if "arwen:" not in loglevel: loglevel += ",arwen:TRACE" if "process/smartcontract:" not in loglevel: loglevel += ",process/smartcontract:TRACE" - loglevel += ",*:TRACE" - return loglevel -def _is_interesting_logline(logline): +LOGLINE_GENESIS_THRESHOLD_MARKER = "started committing block" +LOGLINE_AFTER_GENESIS_INTERESTING_MARKERS = ["started committing block", "ERROR", "WARN", "arwen", "smartcontract"] +# We ignore SC calls on genesis. +LOGLINE_ON_GENESIS_INTERESTING_MARKERS = ["started committing block", "ERROR", "WARN"] + + +def _is_interesting_logline(logline: str): global is_after_genesis - if "started committing block" in logline: + if LOGLINE_GENESIS_THRESHOLD_MARKER in logline: is_after_genesis = True - if not is_after_genesis: - return any(e in logline for e in ["started committing block", "ERROR", "WARN"]) - return any(e in logline for e in ["started committing block", "ERROR", "WARN", "arwen", "smartcontract"]) + if is_after_genesis: + return any(e in logline for e in LOGLINE_AFTER_GENESIS_INTERESTING_MARKERS) + return any(e in logline for e in LOGLINE_ON_GENESIS_INTERESTING_MARKERS) -def _dump_interesting_log_line(pid: str, logline: str): +def _dump_interesting_log_line(pid: int, logline: str): print(f"[PID={pid}]", logline) From b6cc2ee2e76e2bd2776b4285cde29759d859a290 Mon Sep 17 00:00:00 2001 From: Claudiu-Marcel Bruda Date: Wed, 9 Feb 2022 13:47:00 +0200 Subject: [PATCH 19/44] add recursive option for build and clean --- erdpy/cli_contracts.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/erdpy/cli_contracts.py b/erdpy/cli_contracts.py index 5f0fbc2b..3da4e88d 100644 --- a/erdpy/cli_contracts.py +++ b/erdpy/cli_contracts.py @@ -8,6 +8,7 @@ from erdpy.accounts import Account, Address, LedgerAccount from erdpy.contracts import CodeMetadata, SmartContract from erdpy.projects import load_project +from erdpy.projects.core import get_project_paths_recursively from erdpy.proxy.core import ElrondProxy from erdpy.transactions import Transaction @@ -33,6 +34,7 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: sub = cli_shared.add_command_subparser(subparsers, "contract", "build", "Build a Smart Contract project using the appropriate buildchain.") _add_project_arg(sub) + _add_recursive_arg(sub) sub.add_argument("--debug", action="store_true", default=False, help="set debug flag (default: %(default)s)") sub.add_argument("--no-optimization", action="store_true", default=False, help="bypass optimizations (for clang) (default: %(default)s)") @@ -47,6 +49,7 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: sub = cli_shared.add_command_subparser(subparsers, "contract", "clean", "Clean a Smart Contract project.") _add_project_arg(sub) + _add_recursive_arg(sub) sub.set_defaults(func=clean) sub = cli_shared.add_command_subparser(subparsers, "contract", "test", "Run Mandos tests.") @@ -124,6 +127,10 @@ def _add_project_arg(sub: Any): help="🗀 the project directory (default: current directory)") +def _add_recursive_arg(sub: Any): + sub.add_argument("-r", "--recursive", dest="recursive", action="store_true", help="locate projects recursively") + + def _add_project_or_bytecode_arg(sub: Any): group = sub.add_mutually_exclusive_group(required=True) group.add_argument("--project", default=os.getcwd(), @@ -169,13 +176,22 @@ def create(args: Any): projects.create_from_template(name, template, directory) +def get_project_paths(args: Any) -> List[Path]: + base_path = Path(args.project) + recursive = bool(args.recursive) + if not recursive: + return [base_path] + return get_project_paths_recursively(base_path) + + def clean(args: Any): - project = Path(args.project) - projects.clean_project(project) + project_paths = get_project_paths(args) + for project in project_paths: + projects.clean_project(project) def build(args: Any): - project = Path(args.project) + project_paths = get_project_paths(args) options = { "debug": args.debug, "optimized": not args.no_optimization, @@ -186,7 +202,8 @@ def build(args: Any): "wasm_name": args.wasm_name } - projects.build_project(project, options) + for project in project_paths: + projects.build_project(project, options) def run_tests(args: Any): From 039b8930b9a8d3d90e9356ff49251146af144661 Mon Sep 17 00:00:00 2001 From: Claudiu-Marcel Bruda Date: Wed, 9 Feb 2022 14:02:39 +0200 Subject: [PATCH 20/44] log project directory on clean --- erdpy/projects/core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erdpy/projects/core.py b/erdpy/projects/core.py index 47e7a575..c6c09411 100644 --- a/erdpy/projects/core.py +++ b/erdpy/projects/core.py @@ -45,6 +45,7 @@ def build_project(directory: Path, options: Dict[str, Any]): def clean_project(directory: Path): + logger.info("clean_project.directory: %s", directory) directory = directory.expanduser() guards.is_directory(directory) project = load_project(directory) From 3952aa83a34564299491b9bb1aff8fcf6e87217b Mon Sep 17 00:00:00 2001 From: Claudiu-Marcel Bruda Date: Wed, 9 Feb 2022 14:48:20 +0200 Subject: [PATCH 21/44] flip recursive condition --- erdpy/cli_contracts.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erdpy/cli_contracts.py b/erdpy/cli_contracts.py index 3da4e88d..a78543d8 100644 --- a/erdpy/cli_contracts.py +++ b/erdpy/cli_contracts.py @@ -179,9 +179,9 @@ def create(args: Any): def get_project_paths(args: Any) -> List[Path]: base_path = Path(args.project) recursive = bool(args.recursive) - if not recursive: - return [base_path] - return get_project_paths_recursively(base_path) + if recursive: + return get_project_paths_recursively(base_path) + return [base_path] def clean(args: Any): From ec33bb4701cf8b3ac4de5c89c4aeff5a51e2d3fa Mon Sep 17 00:00:00 2001 From: Claudiu-Marcel Bruda Date: Wed, 9 Feb 2022 17:32:49 +0200 Subject: [PATCH 22/44] add twiggy --- erdpy/config.py | 1 + erdpy/dependencies/install.py | 5 +++-- erdpy/dependencies/modules.py | 30 ++++++++++++++++++++++++++++++ erdpy/myprocess.py | 2 +- 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/erdpy/config.py b/erdpy/config.py index a54bf0a1..f61c6ca4 100644 --- a/erdpy/config.py +++ b/erdpy/config.py @@ -176,6 +176,7 @@ def get_defaults() -> Dict[str, Any]: "dependencies.mcl_signer.urlTemplate.linux": "https://github.com/ElrondNetwork/elrond-sdk-go-tools/releases/download/{TAG}/mcl_signer_{TAG}_ubuntu-latest.tar.gz", "dependencies.mcl_signer.urlTemplate.osx": "https://github.com/ElrondNetwork/elrond-sdk-go-tools/releases/download/{TAG}/mcl_signer_{TAG}_macos-latest.tar.gz", "dependencies.wasm-opt.tag": "latest", + "dependencies.twiggy.tag": "latest", "testnet.validate_expected_keys": "false", "github_api_token": "", } diff --git a/erdpy/dependencies/install.py b/erdpy/dependencies/install.py index ec0e529d..8b87d9dd 100644 --- a/erdpy/dependencies/install.py +++ b/erdpy/dependencies/install.py @@ -3,7 +3,7 @@ from typing import Dict, List from erdpy import config, errors -from erdpy.dependencies.modules import (NpmModule, VMToolsModule, DependencyModule, +from erdpy.dependencies.modules import (CargoModule, NpmModule, VMToolsModule, DependencyModule, GolangModule, MclSignerModule, NodejsModule, Rust, StandaloneModule) @@ -63,7 +63,8 @@ def get_all_deps_installable_via_cli() -> List[DependencyModule]: StandaloneModule(key="elrond_go", repo_name="elrond-go", organisation="ElrondNetwork"), StandaloneModule(key="elrond_proxy_go", repo_name="elrond-proxy-go", organisation="ElrondNetwork"), MclSignerModule(key="mcl_signer"), - NpmModule(key="wasm-opt") + NpmModule(key="wasm-opt"), + CargoModule(key="twiggy"), ] diff --git a/erdpy/dependencies/modules.py b/erdpy/dependencies/modules.py index 5465865c..4f0ada2c 100644 --- a/erdpy/dependencies/modules.py +++ b/erdpy/dependencies/modules.py @@ -333,6 +333,36 @@ def get_latest_release(self) -> str: raise errors.UnsupportedConfigurationValue("Rust tag must either be explicit, empty or 'nightly'") +class CargoModule(DependencyModule): + def __init__(self, key: str, aliases: List[str] = None): + if aliases is None: + aliases = list() + + super().__init__(key, aliases) + + def run_command_with_rust_env(self, args: List[str]) -> str: + rust = dependencies.get_module_by_key("rust") + return myprocess.run_process(args, rust.get_env()) + + def _do_install(self, tag: str) -> None: + self.run_command_with_rust_env(["cargo", "install", self.key]) + + def is_installed(self, tag: str) -> bool: + rust = dependencies.get_module_by_key("rust") + output = myprocess.run_process(["cargo", "install", "--list"], rust.get_env()) + for line in output.splitlines(): + if self.key == line.strip(): + return True + return False + + def uninstall(self, tag: str): + if self.is_installed(tag): + self.run_command_with_rust_env(["cargo", "uninstall", self.key]) + + def get_latest_release(self) -> str: + return "latest" + + class MclSignerModule(StandaloneModule): def __init__(self, key: str, aliases: List[str] = None): if aliases is None: diff --git a/erdpy/myprocess.py b/erdpy/myprocess.py index 976f64dd..48ed6132 100644 --- a/erdpy/myprocess.py +++ b/erdpy/myprocess.py @@ -12,7 +12,7 @@ ReturnCode = int -def run_process(args: List[str], env: Any = None, dump_to_stdout: bool = True, cwd: Optional[Union[str, Path]] = None): +def run_process(args: List[str], env: Any = None, dump_to_stdout: bool = True, cwd: Optional[Union[str, Path]] = None) -> str: logger.info(f"run_process: {args}, in folder: {cwd}") try: From f7ca22638c40032bd17d1b31b3270b4895882de6 Mon Sep 17 00:00:00 2001 From: Claudiu-Marcel Bruda Date: Tue, 15 Feb 2022 11:11:42 +0200 Subject: [PATCH 23/44] add `erdpy contract report` command --- erdpy/cli_contracts.py | 11 + erdpy/projects/__init__.py | 3 +- erdpy/projects/project_base.py | 6 + erdpy/projects/project_rust.py | 17 ++ erdpy/projects/report/__init__.py | 1 + erdpy/projects/report/options/__init__.py | 0 erdpy/projects/report/options/builder.py | 11 + .../projects/report/options/report_option.py | 21 ++ erdpy/projects/report/options/size.py | 17 ++ .../report/options/twiggy_paths_check.py | 62 +++++ erdpy/projects/report/report.py | 214 ++++++++++++++++++ 11 files changed, 362 insertions(+), 1 deletion(-) create mode 100644 erdpy/projects/report/__init__.py create mode 100644 erdpy/projects/report/options/__init__.py create mode 100644 erdpy/projects/report/options/builder.py create mode 100644 erdpy/projects/report/options/report_option.py create mode 100644 erdpy/projects/report/options/size.py create mode 100644 erdpy/projects/report/options/twiggy_paths_check.py create mode 100644 erdpy/projects/report/report.py diff --git a/erdpy/cli_contracts.py b/erdpy/cli_contracts.py index a78543d8..4f88f613 100644 --- a/erdpy/cli_contracts.py +++ b/erdpy/cli_contracts.py @@ -59,6 +59,13 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: sub.add_argument("--wildcard", required=False, help="wildcard to match only specific test files") sub.set_defaults(func=run_tests) + sub = cli_shared.add_command_subparser(subparsers, "contract", "report", "Print a detailed report the smart contracts.") + _add_project_arg(sub) + _add_flag(sub, "--skip-build", help="skips the step of building of the wasm contracts") + _add_flag(sub, "--skip-twiggy", help="skips the steps of building the debug wasm files and running twiggy") + sub.add_argument("--output-format", type=str, default="markdown", choices=["markdown", "json"], help="report output format (default: %(default)s)") + sub.set_defaults(func=projects.report_cli) + sub = cli_shared.add_command_subparser(subparsers, "contract", "deploy", "Deploy a Smart Contract.") _add_project_or_bytecode_arg(sub) _add_metadata_arg(sub) @@ -131,6 +138,10 @@ def _add_recursive_arg(sub: Any): sub.add_argument("-r", "--recursive", dest="recursive", action="store_true", help="locate projects recursively") +def _add_flag(sub: Any, flag: str, help: str): + sub.add_argument(flag, action="store_true", default=False, help=help) + + def _add_project_or_bytecode_arg(sub: Any): group = sub.add_mutually_exclusive_group(required=True) group.add_argument("--project", default=os.getcwd(), diff --git a/erdpy/projects/__init__.py b/erdpy/projects/__init__.py index b7e69816..1f061f66 100644 --- a/erdpy/projects/__init__.py +++ b/erdpy/projects/__init__.py @@ -6,7 +6,8 @@ from erdpy.projects.project_cpp import ProjectCpp from erdpy.projects.project_rust import ProjectRust from erdpy.projects.project_sol import ProjectSol +from erdpy.projects.report.report import report_cli from erdpy.projects.templates import (create_from_template, list_project_templates) -__all__ = ["build_project", "clean_project", "run_tests", "get_projects_in_workspace", "load_project", "Project", "ProjectClang", "ProjectCpp", "ProjectRust", "ProjectSol", "create_from_template", "list_project_templates"] +__all__ = ["build_project", "clean_project", "build_report_options", "report_cli", "run_tests", "get_projects_in_workspace", "load_project", "Project", "ProjectClang", "ProjectCpp", "ProjectRust", "ProjectSol", "create_from_template", "list_project_templates"] diff --git a/erdpy/projects/project_base.py b/erdpy/projects/project_base.py index bbda27f4..9673a6c9 100644 --- a/erdpy/projects/project_base.py +++ b/erdpy/projects/project_base.py @@ -60,6 +60,12 @@ def find_file_in_folder(self, folder: Path, pattern: str) -> Path: file = folder / files[0] return Path(file).resolve() + + def find_wasm_files(self): + output_folder = Path(self.get_output_folder()) + wasm_files = output_folder.rglob("*.wasm") + main_wasm_files = list(filter(lambda wasm_path: not wasm_path.name.endswith("-dbg.wasm"), wasm_files)) + return main_wasm_files def _do_after_build(self) -> List[Path]: raise NotImplementedError() diff --git a/erdpy/projects/project_rust.py b/erdpy/projects/project_rust.py index 5c349259..84e057d8 100644 --- a/erdpy/projects/project_rust.py +++ b/erdpy/projects/project_rust.py @@ -167,6 +167,23 @@ def get_dependencies(self): def get_env(self): return dependencies.get_module_by_key("rust").get_env() + def build_wasm_with_debug_symbols(self): + cwd = self.get_meta_folder() + env = self.get_env() + + args = [ + "cargo", + "run", + "build", + "--wasm-symbols", + "--wasm-suffix", "dbg", + "--no-wasm-opt" + ] + + return_code = myprocess.run_process_async(args, env=env, cwd=str(cwd)) + if return_code != 0: + raise errors.BuildError(f"error code = {return_code}, see output") + class CargoFile: data: Dict[str, Any] diff --git a/erdpy/projects/report/__init__.py b/erdpy/projects/report/__init__.py new file mode 100644 index 00000000..2e197f23 --- /dev/null +++ b/erdpy/projects/report/__init__.py @@ -0,0 +1 @@ +from .report import report_cli diff --git a/erdpy/projects/report/options/__init__.py b/erdpy/projects/report/options/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/erdpy/projects/report/options/builder.py b/erdpy/projects/report/options/builder.py new file mode 100644 index 00000000..068b07f6 --- /dev/null +++ b/erdpy/projects/report/options/builder.py @@ -0,0 +1,11 @@ +from typing import List +from .report_option import ReportOption +from .size import Size +from .twiggy_paths_check import TwiggyPathsCheck + +def get_default_report_options() -> List[ReportOption]: + return [ + Size("size"), + TwiggyPathsCheck("has-allocator", pattern="wee_alloc::"), + TwiggyPathsCheck("has-format", pattern="core::fmt"), + ] diff --git a/erdpy/projects/report/options/report_option.py b/erdpy/projects/report/options/report_option.py new file mode 100644 index 00000000..05b746d6 --- /dev/null +++ b/erdpy/projects/report/options/report_option.py @@ -0,0 +1,21 @@ +from abc import abstractmethod +from pathlib import Path +from typing import Any, Optional + + +class ReportOption: + def __init__(self, name: str) -> None: + self.name = name + + @abstractmethod + def apply(self, wasm_path: Path) -> str: + pass + + def requires_twiggy_paths(self) -> bool: + return False + + +def str_or_default(field: Optional[Any], default: str = '-') -> str: + if field is None: + return default + return str(field) diff --git a/erdpy/projects/report/options/size.py b/erdpy/projects/report/options/size.py new file mode 100644 index 00000000..fea10ceb --- /dev/null +++ b/erdpy/projects/report/options/size.py @@ -0,0 +1,17 @@ +from pathlib import Path +from typing import Optional + +from .report_option import ReportOption, str_or_default + + +class Size(ReportOption): + def apply(self, wasm_path: Path): + size = get_file_size(wasm_path) + return str_or_default(size) + + +def get_file_size(file_path: Path) -> Optional[int]: + try: + return int(file_path.stat().st_size) + except FileNotFoundError: + return None diff --git a/erdpy/projects/report/options/twiggy_paths_check.py b/erdpy/projects/report/options/twiggy_paths_check.py new file mode 100644 index 00000000..af6a6840 --- /dev/null +++ b/erdpy/projects/report/options/twiggy_paths_check.py @@ -0,0 +1,62 @@ +import logging +from pathlib import Path +from erdpy import dependencies, myprocess, utils +from erdpy.errors import BadFile + +from erdpy.projects.project_base import remove_suffix + +from .report_option import ReportOption + + +logger = logging.getLogger("projects.report.options.twiggy_paths_check") + + +class TwiggyPathsCheck(ReportOption): + def __init__(self, name: str, pattern: str) -> None: + super().__init__(name) + + self.pattern = pattern + + + def apply(self, wasm_path: Path) -> str: + twiggy_paths_path = get_twiggy_paths_path(wasm_path) + try: + text = utils.read_text_file(twiggy_paths_path) + return str(self.pattern in text) + except BadFile: + return 'N/A' + + def requires_twiggy_paths(self): + return True + + +def replace_file_suffix(file_path: Path, suffix: str) -> Path: + new_name = file_path.stem + suffix + return file_path.with_name(new_name) + + +def get_debug_wasm_path(wasm_path: Path) -> Path: + """ +>>> get_debug_wasm_path(Path('test/contract.wasm')) +PosixPath('test/contract-dbg.wasm') + """ + return replace_file_suffix(wasm_path, '-dbg.wasm') + + +def get_twiggy_paths_path(wasm_path: Path) -> Path: + """ +>>> replace_file_suffix(Path('test/contract.wasm'), '-paths.txt') +PosixPath('test/contract-paths.txt') + """ + return replace_file_suffix(wasm_path, '-paths.txt') + + +def run_twiggy_paths(wasm_path: Path) -> Path: + rust = dependencies.get_module_by_key("rust") + debug_wasm_path = get_debug_wasm_path(wasm_path) + twiggy_paths_args = ["twiggy", "paths", str(debug_wasm_path)] + output = myprocess.run_process(twiggy_paths_args, env=rust.get_env(), cwd=debug_wasm_path.parent, dump_to_stdout=False) + output_path = get_twiggy_paths_path(wasm_path) + utils.write_file(output_path, output) + logger.info(f"Twiggy paths output path: {output_path}") + return output_path diff --git a/erdpy/projects/report/report.py b/erdpy/projects/report/report.py new file mode 100644 index 00000000..36beea38 --- /dev/null +++ b/erdpy/projects/report/report.py @@ -0,0 +1,214 @@ +from io import StringIO +import itertools +import json +import logging +import operator +from pathlib import Path +from typing import Any, Iterable, List, Tuple + +from erdpy import guards +from erdpy.projects.core import get_project_paths_recursively, load_project +from erdpy.projects.project_base import remove_suffix +from erdpy.projects.project_rust import ProjectRust +from erdpy.projects.report.options.builder import get_default_report_options +from erdpy.projects.report.options.report_option import ReportOption +from erdpy.projects.report.options.twiggy_paths_check import run_twiggy_paths + + +logger = logging.getLogger("report") + +def print_strings(strings: List[str]) -> None: + joined_strings = " ".join(strings) + print(joined_strings) + + +def group_projects_by_folder(project_paths: List[Path]) -> Iterable[Tuple[Path, Iterable[Tuple[Path, Path]]]]: + path_pairs = sorted([(path.parent, path) for path in project_paths]) + return itertools.groupby(path_pairs, operator.itemgetter(0)) + + +class OptionResult: + def __init__(self, option_name: str, result: str) -> None: + self.option_name = option_name + self.result = result + + def toJson(self) -> Any: + return { + 'option_name': self.option_name, + 'result': self.result + } + +class WasmReport: + def __init__(self, wasm_name: str, option_results: List[OptionResult]) -> None: + self.wasm_name = wasm_name + self.option_results = option_results + + def toJson(self) -> Any: + return { + 'wasm_name': self.wasm_name, + 'option_results': self.option_results + } + + def get_option_results(self) -> List[str]: + return [option.result for option in self.option_results] + + +class ProjectReport: + def __init__(self, project_path: Path, wasms: List[WasmReport]) -> None: + self.project_path = project_path + self.wasms = wasms + + def toJson(self) -> Any: + return { + "project_path": str(self.project_path), + 'wasms': self.wasms + } + + def get_rows(self) -> List[List[str]]: + wasm_count = len(self.wasms) + if wasm_count == 0: + return [[f" - {str(self.project_path)} "]] + elif wasm_count == 1: + return [[f" - {str(self.project_path / self.wasms[0].wasm_name)}"] + self.wasms[0].get_option_results()] + else: + project_path_row = [f" - {str(self.project_path)}"] + wasm_rows = [[f" - - {wasm.wasm_name}"] + wasm.get_option_results() for wasm in self.wasms] + return [project_path_row] + wasm_rows + + +class FolderReport: + def __init__(self, root_path: Path, projects: List[ProjectReport]) -> None: + self.root_path = root_path + self.projects = projects + + def toJson(self) -> Any: + return { + 'root_path': str(self.root_path), + 'projects': self.projects + } + + def get_rows(self) -> List[List[str]]: + folder_row = [str(self.root_path)] + project_rows = flatten_list_of_rows([project.get_rows() for project in self.projects]) + return [folder_row] + project_rows + + +def flatten_list_of_rows(list_of_rows: List[List[List[str]]]) -> List[List[str]]: + return list(itertools.chain(*list_of_rows)) + +def format_row_markdown(row: List[str]) -> str: + row += [''] * (4 - len(row)) + row[0] = row[0].ljust(80) + row[1] = row[1].rjust(15) + row[2] = row[2].rjust(15) + row[3] = row[3].rjust(15) + merged_cells = " | ".join(row) + return f"| {merged_cells} |" + +def write_markdown_row(string: StringIO, row: List[str]): + string.write(format_row_markdown(row)) + string.write('\n') + +class Report: + def __init__(self, options: List[ReportOption], folders: List[FolderReport]) -> None: + self.option_names = [option.name for option in options] + self.folders = folders + + def toJson(self) -> Any: + return { + 'options': self.option_names, + 'folders': self.folders + } + + def get_rows(self) -> List[List[str]]: + rows = [group.get_rows() for group in self.folders] + return flatten_list_of_rows(rows) + + def toMarkdown(self) -> str: + string = StringIO() + + table_header = ["Path"] + self.option_names + write_markdown_row(string, table_header) + + ALIGN_LEFT = ":--" + ALIGN_RIGHT = "--:" + row_alignments = [ALIGN_LEFT] + len(self.option_names) * [ALIGN_RIGHT] + write_markdown_row(string, row_alignments) + + for row in self.get_rows(): + write_markdown_row(string, row) + + return string.getvalue() + + def toJsonString(self) -> str: + return json.dumps(self, indent=4, default=lambda obj: obj.toJson()) + + +class ReportCreator: + def __init__(self, options: List[ReportOption], skip_build: bool, skip_twiggy: bool) -> None: + self.options = options + self.skip_build = skip_build + self.skip_twiggy = skip_twiggy + self.require_twiggy_paths = any(option.requires_twiggy_paths() for option in self.options) + + + def apply_option(self, option: ReportOption, wasm_path: Path) -> OptionResult: + result = option.apply(wasm_path) + return OptionResult(option.name, result) + + + def create_wasm_report(self, wasm_path: Path, twiggy_requirements_met: bool) -> WasmReport: + if twiggy_requirements_met: + run_twiggy_paths(wasm_path) + name = wasm_path.name + option_results = [self.apply_option(option, wasm_path) for option in self.options] + return WasmReport(name, option_results) + + + def create_project_report(self, parent_path: Path, project_path: Path) -> ProjectReport: + project_path = project_path.resolve() + project = load_project(project_path) + + if not self.skip_build: + project.build() + + twiggy_requirements_met = False + should_build_twiggy = self.require_twiggy_paths and not self.skip_twiggy + if should_build_twiggy and isinstance(project, ProjectRust): + project.build_wasm_with_debug_symbols() + twiggy_requirements_met = True + + wasm_reports = [self.create_wasm_report(wasm_path, twiggy_requirements_met) for wasm_path in project.find_wasm_files()] + wasm_reports.sort(key=lambda report: remove_suffix(report.wasm_name, '.wasm')) + + return ProjectReport(project_path.relative_to(parent_path), wasm_reports) + + + def create_folder_report(self, base_path: Path, parent_folder: Path, iter: Iterable[Tuple[Path, Path]]) -> FolderReport: + parent_folder = parent_folder.resolve() + project_reports = [self.create_project_report(parent_folder, project_path) for _, project_path in iter] + + root_path = parent_folder.relative_to(base_path) + return FolderReport(root_path, project_reports) + + + def create_report(self, base_path: Path, project_paths: List[Path]) -> Report: + base_path = base_path.resolve() + guards.is_directory(base_path) + + folder_groups = [self.create_folder_report(base_path, parent_folder, iter) + for parent_folder, iter in group_projects_by_folder(project_paths)] + + return Report(self.options, folder_groups) + + +def report_cli(args: Any) -> None: + base_path = Path(args.project) + project_paths = get_project_paths_recursively(base_path) + options = get_default_report_options() + report_creator = ReportCreator(options, skip_build=args.skip_build, skip_twiggy=args.skip_twiggy) + report = report_creator.create_report(base_path, project_paths) + if args.output_format == "markdown": + print(report.toMarkdown()) + elif args.output_format == "json": + print(report.toJsonString()) From ac7df5ef5afccf7075a63c5ec06673f557a0ebcf Mon Sep 17 00:00:00 2001 From: Claudiu-Marcel Bruda Date: Tue, 15 Feb 2022 11:27:34 +0200 Subject: [PATCH 24/44] fix crash when current dir is a project --- erdpy/projects/report/report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erdpy/projects/report/report.py b/erdpy/projects/report/report.py index 36beea38..3ae5eb3a 100644 --- a/erdpy/projects/report/report.py +++ b/erdpy/projects/report/report.py @@ -188,7 +188,7 @@ def create_folder_report(self, base_path: Path, parent_folder: Path, iter: Itera parent_folder = parent_folder.resolve() project_reports = [self.create_project_report(parent_folder, project_path) for _, project_path in iter] - root_path = parent_folder.relative_to(base_path) + root_path = parent_folder.relative_to(base_path.parent) return FolderReport(root_path, project_reports) From 9fe6a60569ae1875372094b1026ad88bf8b6c99a Mon Sep 17 00:00:00 2001 From: Claudiu-Marcel Bruda Date: Tue, 22 Feb 2022 20:30:30 +0200 Subject: [PATCH 25/44] re-organize, add merging --- erdpy/cli_contracts.py | 2 + erdpy/projects/__init__.py | 2 +- erdpy/projects/report/__init__.py | 2 +- erdpy/projects/report/data/__init__.py | 0 erdpy/projects/report/data/common.py | 40 ++++ erdpy/projects/report/data/folder_report.py | 41 ++++ erdpy/projects/report/data/option_results.py | 43 ++++ erdpy/projects/report/data/project_report.py | 48 +++++ erdpy/projects/report/data/report.py | 88 ++++++++ erdpy/projects/report/data/wasm_report.py | 38 ++++ erdpy/projects/report/report.py | 214 ------------------- erdpy/projects/report/report_cli.py | 53 +++++ erdpy/projects/report/report_creator.py | 81 +++++++ 13 files changed, 436 insertions(+), 216 deletions(-) create mode 100644 erdpy/projects/report/data/__init__.py create mode 100644 erdpy/projects/report/data/common.py create mode 100644 erdpy/projects/report/data/folder_report.py create mode 100644 erdpy/projects/report/data/option_results.py create mode 100644 erdpy/projects/report/data/project_report.py create mode 100644 erdpy/projects/report/data/report.py create mode 100644 erdpy/projects/report/data/wasm_report.py delete mode 100644 erdpy/projects/report/report.py create mode 100644 erdpy/projects/report/report_cli.py create mode 100644 erdpy/projects/report/report_creator.py diff --git a/erdpy/cli_contracts.py b/erdpy/cli_contracts.py index 4f88f613..beda1df5 100644 --- a/erdpy/cli_contracts.py +++ b/erdpy/cli_contracts.py @@ -64,6 +64,8 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: _add_flag(sub, "--skip-build", help="skips the step of building of the wasm contracts") _add_flag(sub, "--skip-twiggy", help="skips the steps of building the debug wasm files and running twiggy") sub.add_argument("--output-format", type=str, default="markdown", choices=["markdown", "json"], help="report output format (default: %(default)s)") + sub.add_argument("--output-file", type=Path, help="if specified, the output is written to a file, otherwise it's written to the standard output") + sub.add_argument("--compare", type=Path, nargs='+', metavar=("report-1.json", "report-2.json"), help="create a comparison from two or more reports") sub.set_defaults(func=projects.report_cli) sub = cli_shared.add_command_subparser(subparsers, "contract", "deploy", "Deploy a Smart Contract.") diff --git a/erdpy/projects/__init__.py b/erdpy/projects/__init__.py index 1f061f66..a8e1af69 100644 --- a/erdpy/projects/__init__.py +++ b/erdpy/projects/__init__.py @@ -6,7 +6,7 @@ from erdpy.projects.project_cpp import ProjectCpp from erdpy.projects.project_rust import ProjectRust from erdpy.projects.project_sol import ProjectSol -from erdpy.projects.report.report import report_cli +from erdpy.projects.report.report_cli import report_cli from erdpy.projects.templates import (create_from_template, list_project_templates) diff --git a/erdpy/projects/report/__init__.py b/erdpy/projects/report/__init__.py index 2e197f23..2f67cd2a 100644 --- a/erdpy/projects/report/__init__.py +++ b/erdpy/projects/report/__init__.py @@ -1 +1 @@ -from .report import report_cli +from .report_cli import report_cli diff --git a/erdpy/projects/report/data/__init__.py b/erdpy/projects/report/data/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/erdpy/projects/report/data/common.py b/erdpy/projects/report/data/common.py new file mode 100644 index 00000000..f8ce6511 --- /dev/null +++ b/erdpy/projects/report/data/common.py @@ -0,0 +1,40 @@ +from collections import OrderedDict +import itertools +from typing import Callable, List, Optional, TypeVar + + +def flatten_list_of_rows(list_of_rows: List[List[List[str]]]) -> List[List[str]]: + return list(itertools.chain(*list_of_rows)) + + +def merge_values(first: List[str], second: List[str]) -> List[str]: + return list(OrderedDict.fromkeys(first + second)) + + +T = TypeVar('T') +K = TypeVar('K') + + +def first_non_none(first: Optional[T], second: Optional[T]) -> T: + return next(item for item in [first, second] if item is not None) + + +def get_keys(items: List[T], key_getter: Callable[[T], K]) -> List[K]: + return [key_getter(item) for item in items] + + +def list_as_key_value_dict(items: List[T], key_getter: Callable[[T], K]) -> 'OrderedDict[K, T]': + return OrderedDict(zip(get_keys(items, key_getter), items)) + + +def merge_values_by_key(first: List[T], second: List[T], key_getter: Callable[[T], K], merge: Callable[[Optional[T], Optional[T]], T]) -> List[T]: + first_as_dict = list_as_key_value_dict(first, key_getter) + second_as_dict = list_as_key_value_dict(second, key_getter) + union = OrderedDict.fromkeys(list(first_as_dict.keys()) + list(second_as_dict.keys())) + all_keys = union.keys() + result = [] + for key in all_keys: + first_value = first_as_dict.get(key) + second_value = second_as_dict.get(key) + result.append(merge(first_value, second_value)) + return result diff --git a/erdpy/projects/report/data/folder_report.py b/erdpy/projects/report/data/folder_report.py new file mode 100644 index 00000000..337840ce --- /dev/null +++ b/erdpy/projects/report/data/folder_report.py @@ -0,0 +1,41 @@ +from pathlib import Path +from typing import Any, List, Optional +from erdpy.projects.report.data.common import first_non_none, flatten_list_of_rows, merge_values_by_key + +from erdpy.projects.report.data.project_report import ProjectReport, merge_list_of_projects + + +class FolderReport: + def __init__(self, root_path: Path, projects: List[ProjectReport]) -> None: + self.root_path = root_path + self.projects = projects + + def to_json(self) -> Any: + return { + 'root_path': str(self.root_path), + 'projects': self.projects + } + + def from_json(json: Any) -> 'FolderReport': + projects = [ProjectReport.from_json(project) for project in json['projects']] + return FolderReport(Path(json['root_path']), projects) + + def get_rows(self) -> List[List[str]]: + folder_row = [str(self.root_path)] + project_rows = flatten_list_of_rows([project.get_rows_markdown() for project in self.projects]) + return [folder_row] + project_rows + + +def merge_list_of_folder_reports(first: List[FolderReport], second: List[FolderReport]) -> List[FolderReport]: + return merge_values_by_key(first, second, get_folder_report_root_path, merge_two_folder_reports) + + +def get_folder_report_root_path(item: FolderReport) -> Path: + return item.root_path + + +def merge_two_folder_reports(first: Optional[FolderReport], second: Optional[FolderReport]) -> FolderReport: + if first is None or second is None: + return first_non_none(first, second) + merged_projects = merge_list_of_projects(first.projects, second.projects) + return FolderReport(first.root_path, merged_projects) diff --git a/erdpy/projects/report/data/option_results.py b/erdpy/projects/report/data/option_results.py new file mode 100644 index 00000000..bf467cb2 --- /dev/null +++ b/erdpy/projects/report/data/option_results.py @@ -0,0 +1,43 @@ +from typing import Any, List, Optional + +from erdpy.projects.report.data.common import first_non_none, merge_values_by_key + + +class OptionResults: + def __init__(self, option_name: str, results: List[str]) -> None: + self.option_name = option_name + self.results = results + + def to_json(self) -> Any: + return { + 'option_name': self.option_name, + 'results': self.results + } + + @staticmethod + def from_json(json: Any) -> 'OptionResults': + return OptionResults(json['option_name'], json['results']) + + def results_to_markdown(self) -> str: + return ' -> '.join(self.results) + + +def merge_lists_of_option_results(first: List[OptionResults], second: List[OptionResults]) -> List[OptionResults]: + return merge_values_by_key(first, second, get_option_result_key, merge_two_option_results) + + +def get_option_result_key(option_results: OptionResults) -> str: + return option_results.option_name + + +def merge_two_option_results(first: Optional[OptionResults], second: Optional[OptionResults]) -> OptionResults: + any = first_non_none(first, second) + merged_results = results_or_NA(first) + results_or_NA(second) + return OptionResults(any.option_name, merged_results) + + +def results_or_NA(option_result: Optional[OptionResults]) -> List[str]: + if option_result is None: + return ['N/A'] + else: + return option_result.results diff --git a/erdpy/projects/report/data/project_report.py b/erdpy/projects/report/data/project_report.py new file mode 100644 index 00000000..ec7e19c2 --- /dev/null +++ b/erdpy/projects/report/data/project_report.py @@ -0,0 +1,48 @@ +from pathlib import Path +from typing import Any, List, Optional +from erdpy.projects.report.data.common import first_non_none, merge_values_by_key + +from erdpy.projects.report.data.wasm_report import WasmReport, merge_list_of_wasms + + +class ProjectReport: + def __init__(self, project_path: Path, wasms: List[WasmReport]) -> None: + self.project_path = project_path + self.wasms = wasms + + def to_json(self) -> Any: + return { + "project_path": str(self.project_path), + 'wasms': self.wasms + } + + @staticmethod + def from_json(json: Any) -> 'ProjectReport': + wasms = [WasmReport.from_json(wasm) for wasm in json['wasms']] + return ProjectReport(Path(json['project_path']), wasms) + + def get_rows_markdown(self) -> List[List[str]]: + wasm_count = len(self.wasms) + if wasm_count == 0: + return [[f" - {str(self.project_path)} "]] + elif wasm_count == 1: + return [[f" - {str(self.project_path / self.wasms[0].wasm_name)}"] + self.wasms[0].get_option_results()] + else: + project_path_row = [f" - {str(self.project_path)}"] + wasm_rows = [[f" - - {wasm.wasm_name}"] + wasm.get_option_results() for wasm in self.wasms] + return [project_path_row] + wasm_rows + + +def merge_list_of_projects(first: List[ProjectReport], second: List[ProjectReport]) -> List[ProjectReport]: + return merge_values_by_key(first, second, get_project_report_path, merge_two_project_reports) + + +def get_project_report_path(project_report: ProjectReport) -> Path: + return project_report.project_path + + +def merge_two_project_reports(first: Optional[ProjectReport], second: Optional[ProjectReport]) -> ProjectReport: + if first is None or second is None: + return first_non_none(first, second) + merged_wasms = merge_list_of_wasms(first.wasms, second.wasms) + return ProjectReport(first.project_path, merged_wasms) diff --git a/erdpy/projects/report/data/report.py b/erdpy/projects/report/data/report.py new file mode 100644 index 00000000..7693d68d --- /dev/null +++ b/erdpy/projects/report/data/report.py @@ -0,0 +1,88 @@ +import functools +from io import StringIO +import json +from pathlib import Path +from typing import Any, List +from erdpy.projects.report.data.folder_report import FolderReport, merge_list_of_folder_reports + +from erdpy.projects.report.data.common import flatten_list_of_rows, merge_values, merge_values_by_key +from erdpy.projects.report.data.option_results import OptionResults +from erdpy.projects.report.data.project_report import ProjectReport +from erdpy.projects.report.data.wasm_report import WasmReport + + +class Report: + def __init__(self, option_names: List[str], folders: List[FolderReport]) -> None: + self.option_names = option_names + self.folders = folders + + + def to_json(self) -> Any: + return { + 'options': self.option_names, + 'folders': self.folders + } + + + @staticmethod + def from_json(json: Any) -> 'Report': + folders = [FolderReport.from_json(folder_report) for folder_report in json['folders']] + return Report(json['options'], folders) + + + @staticmethod + def load_from_file(report_json_path: Path) -> 'Report': + with open(report_json_path, 'r') as report_file: + report_json = json.load(report_file) + return Report.from_json(report_json) + + + def get_rows(self) -> List[List[str]]: + rows = [group.get_rows() for group in self.folders] + return flatten_list_of_rows(rows) + + + def to_markdown(self) -> str: + string = StringIO() + + table_header = ["Path"] + self.option_names + write_markdown_row(string, table_header) + + ALIGN_LEFT = ":--" + ALIGN_RIGHT = "--:" + row_alignments = [ALIGN_LEFT] + len(self.option_names) * [ALIGN_RIGHT] + write_markdown_row(string, row_alignments) + + for row in self.get_rows(): + write_markdown_row(string, row) + + return string.getvalue() + + + def to_json_string(self) -> str: + return json.dumps(self, indent=4, default=lambda obj: obj.to_json()) + + +def merge_list_of_reports(reports: List[Report]) -> Report: + return functools.reduce(merge_two_reports, reports) + + +def merge_two_reports(first: Report, other: Report) -> Report: + option_names = merge_values(first.option_names, other.option_names) + folders = merge_list_of_folder_reports(first.folders, other.folders) + return Report(option_names, folders) + + +def format_row_markdown(row: List[str]) -> str: + row += [''] * (4 - len(row)) + row[0] = row[0].ljust(80) + row[1] = row[1].rjust(15) + row[2] = row[2].rjust(15) + row[3] = row[3].rjust(15) + merged_cells = " | ".join(row) + return f"| {merged_cells} |" + + +def write_markdown_row(string: StringIO, row: List[str]): + string.write(format_row_markdown(row)) + string.write('\n') diff --git a/erdpy/projects/report/data/wasm_report.py b/erdpy/projects/report/data/wasm_report.py new file mode 100644 index 00000000..b2713387 --- /dev/null +++ b/erdpy/projects/report/data/wasm_report.py @@ -0,0 +1,38 @@ +from typing import Any, List, Optional +from erdpy.projects.report.data.common import first_non_none, merge_values_by_key +from erdpy.projects.report.data.option_results import OptionResults, merge_lists_of_option_results + + +class WasmReport: + def __init__(self, wasm_name: str, option_results: List[OptionResults]) -> None: + self.wasm_name = wasm_name + self.option_results = option_results + + def to_json(self) -> Any: + return { + 'wasm_name': self.wasm_name, + 'option_results': self.option_results + } + + @staticmethod + def from_json(json: Any) -> 'WasmReport': + option_results = [OptionResults.from_json(option_result) for option_result in json['option_results']] + return WasmReport(json['wasm_name'], option_results) + + def get_option_results(self) -> List[str]: + return [option.results_to_markdown() for option in self.option_results] + + +def merge_list_of_wasms(first: List[WasmReport], second: List[WasmReport]) -> List[WasmReport]: + return merge_values_by_key(first, second, get_wasm_key, merge_two_wasms) + + +def get_wasm_key(wasm: WasmReport) -> str: + return wasm.wasm_name + + +def merge_two_wasms(first: Optional[WasmReport], second: Optional[WasmReport]) -> WasmReport: + if first is None or second is None: + return first_non_none(first, second) + merged_option_results = merge_lists_of_option_results(first.option_results, second.option_results) + return WasmReport(first.wasm_name, merged_option_results) diff --git a/erdpy/projects/report/report.py b/erdpy/projects/report/report.py deleted file mode 100644 index 3ae5eb3a..00000000 --- a/erdpy/projects/report/report.py +++ /dev/null @@ -1,214 +0,0 @@ -from io import StringIO -import itertools -import json -import logging -import operator -from pathlib import Path -from typing import Any, Iterable, List, Tuple - -from erdpy import guards -from erdpy.projects.core import get_project_paths_recursively, load_project -from erdpy.projects.project_base import remove_suffix -from erdpy.projects.project_rust import ProjectRust -from erdpy.projects.report.options.builder import get_default_report_options -from erdpy.projects.report.options.report_option import ReportOption -from erdpy.projects.report.options.twiggy_paths_check import run_twiggy_paths - - -logger = logging.getLogger("report") - -def print_strings(strings: List[str]) -> None: - joined_strings = " ".join(strings) - print(joined_strings) - - -def group_projects_by_folder(project_paths: List[Path]) -> Iterable[Tuple[Path, Iterable[Tuple[Path, Path]]]]: - path_pairs = sorted([(path.parent, path) for path in project_paths]) - return itertools.groupby(path_pairs, operator.itemgetter(0)) - - -class OptionResult: - def __init__(self, option_name: str, result: str) -> None: - self.option_name = option_name - self.result = result - - def toJson(self) -> Any: - return { - 'option_name': self.option_name, - 'result': self.result - } - -class WasmReport: - def __init__(self, wasm_name: str, option_results: List[OptionResult]) -> None: - self.wasm_name = wasm_name - self.option_results = option_results - - def toJson(self) -> Any: - return { - 'wasm_name': self.wasm_name, - 'option_results': self.option_results - } - - def get_option_results(self) -> List[str]: - return [option.result for option in self.option_results] - - -class ProjectReport: - def __init__(self, project_path: Path, wasms: List[WasmReport]) -> None: - self.project_path = project_path - self.wasms = wasms - - def toJson(self) -> Any: - return { - "project_path": str(self.project_path), - 'wasms': self.wasms - } - - def get_rows(self) -> List[List[str]]: - wasm_count = len(self.wasms) - if wasm_count == 0: - return [[f" - {str(self.project_path)} "]] - elif wasm_count == 1: - return [[f" - {str(self.project_path / self.wasms[0].wasm_name)}"] + self.wasms[0].get_option_results()] - else: - project_path_row = [f" - {str(self.project_path)}"] - wasm_rows = [[f" - - {wasm.wasm_name}"] + wasm.get_option_results() for wasm in self.wasms] - return [project_path_row] + wasm_rows - - -class FolderReport: - def __init__(self, root_path: Path, projects: List[ProjectReport]) -> None: - self.root_path = root_path - self.projects = projects - - def toJson(self) -> Any: - return { - 'root_path': str(self.root_path), - 'projects': self.projects - } - - def get_rows(self) -> List[List[str]]: - folder_row = [str(self.root_path)] - project_rows = flatten_list_of_rows([project.get_rows() for project in self.projects]) - return [folder_row] + project_rows - - -def flatten_list_of_rows(list_of_rows: List[List[List[str]]]) -> List[List[str]]: - return list(itertools.chain(*list_of_rows)) - -def format_row_markdown(row: List[str]) -> str: - row += [''] * (4 - len(row)) - row[0] = row[0].ljust(80) - row[1] = row[1].rjust(15) - row[2] = row[2].rjust(15) - row[3] = row[3].rjust(15) - merged_cells = " | ".join(row) - return f"| {merged_cells} |" - -def write_markdown_row(string: StringIO, row: List[str]): - string.write(format_row_markdown(row)) - string.write('\n') - -class Report: - def __init__(self, options: List[ReportOption], folders: List[FolderReport]) -> None: - self.option_names = [option.name for option in options] - self.folders = folders - - def toJson(self) -> Any: - return { - 'options': self.option_names, - 'folders': self.folders - } - - def get_rows(self) -> List[List[str]]: - rows = [group.get_rows() for group in self.folders] - return flatten_list_of_rows(rows) - - def toMarkdown(self) -> str: - string = StringIO() - - table_header = ["Path"] + self.option_names - write_markdown_row(string, table_header) - - ALIGN_LEFT = ":--" - ALIGN_RIGHT = "--:" - row_alignments = [ALIGN_LEFT] + len(self.option_names) * [ALIGN_RIGHT] - write_markdown_row(string, row_alignments) - - for row in self.get_rows(): - write_markdown_row(string, row) - - return string.getvalue() - - def toJsonString(self) -> str: - return json.dumps(self, indent=4, default=lambda obj: obj.toJson()) - - -class ReportCreator: - def __init__(self, options: List[ReportOption], skip_build: bool, skip_twiggy: bool) -> None: - self.options = options - self.skip_build = skip_build - self.skip_twiggy = skip_twiggy - self.require_twiggy_paths = any(option.requires_twiggy_paths() for option in self.options) - - - def apply_option(self, option: ReportOption, wasm_path: Path) -> OptionResult: - result = option.apply(wasm_path) - return OptionResult(option.name, result) - - - def create_wasm_report(self, wasm_path: Path, twiggy_requirements_met: bool) -> WasmReport: - if twiggy_requirements_met: - run_twiggy_paths(wasm_path) - name = wasm_path.name - option_results = [self.apply_option(option, wasm_path) for option in self.options] - return WasmReport(name, option_results) - - - def create_project_report(self, parent_path: Path, project_path: Path) -> ProjectReport: - project_path = project_path.resolve() - project = load_project(project_path) - - if not self.skip_build: - project.build() - - twiggy_requirements_met = False - should_build_twiggy = self.require_twiggy_paths and not self.skip_twiggy - if should_build_twiggy and isinstance(project, ProjectRust): - project.build_wasm_with_debug_symbols() - twiggy_requirements_met = True - - wasm_reports = [self.create_wasm_report(wasm_path, twiggy_requirements_met) for wasm_path in project.find_wasm_files()] - wasm_reports.sort(key=lambda report: remove_suffix(report.wasm_name, '.wasm')) - - return ProjectReport(project_path.relative_to(parent_path), wasm_reports) - - - def create_folder_report(self, base_path: Path, parent_folder: Path, iter: Iterable[Tuple[Path, Path]]) -> FolderReport: - parent_folder = parent_folder.resolve() - project_reports = [self.create_project_report(parent_folder, project_path) for _, project_path in iter] - - root_path = parent_folder.relative_to(base_path.parent) - return FolderReport(root_path, project_reports) - - - def create_report(self, base_path: Path, project_paths: List[Path]) -> Report: - base_path = base_path.resolve() - guards.is_directory(base_path) - - folder_groups = [self.create_folder_report(base_path, parent_folder, iter) - for parent_folder, iter in group_projects_by_folder(project_paths)] - - return Report(self.options, folder_groups) - - -def report_cli(args: Any) -> None: - base_path = Path(args.project) - project_paths = get_project_paths_recursively(base_path) - options = get_default_report_options() - report_creator = ReportCreator(options, skip_build=args.skip_build, skip_twiggy=args.skip_twiggy) - report = report_creator.create_report(base_path, project_paths) - if args.output_format == "markdown": - print(report.toMarkdown()) - elif args.output_format == "json": - print(report.toJsonString()) diff --git a/erdpy/projects/report/report_cli.py b/erdpy/projects/report/report_cli.py new file mode 100644 index 00000000..1dedd680 --- /dev/null +++ b/erdpy/projects/report/report_cli.py @@ -0,0 +1,53 @@ +import logging +from pathlib import Path +from typing import Any, List +from erdpy import utils + +from erdpy.projects.core import get_project_paths_recursively +from erdpy.projects.report.data.report import Report, merge_list_of_reports, merge_two_reports +from erdpy.projects.report.options.builder import get_default_report_options +from erdpy.projects.report.report_creator import ReportCreator + + +logger = logging.getLogger("report") + +def report_cli(args: Any) -> None: + compare_report_paths = args.compare + if compare_report_paths is None: + build_report_cli(args) + else: + compare_reports_cli(args, compare_report_paths) + + +def build_report_cli(args: Any) -> None: + base_path = Path(args.project) + project_paths = get_project_paths_recursively(base_path) + options = get_default_report_options() + report_creator = ReportCreator(options, skip_build=args.skip_build, skip_twiggy=args.skip_twiggy) + report = report_creator.create_report(base_path, project_paths) + finalize_report(report, args) + + +def compare_reports_cli(args: Any, merge_report_paths: List[Path]) -> None: + reports = [Report.load_from_file(report_path) for report_path in merge_report_paths] + final_report = merge_list_of_reports(reports) + finalize_report(final_report, args) + + +def finalize_report(report: Report, args: Any) -> None: + output = get_report_output_string(report, args) + store_output(output, args) + +def get_report_output_string(report: Report, args: Any) -> str: + if args.output_format == "markdown": + return report.to_markdown() + elif args.output_format == "json": + return report.to_json_string() + raise Exception('Invalid output format') + +def store_output(output: str, args: Any) -> None: + output_file_path = args.output_file + if output_file_path is None: + print(output) + else: + utils.write_file(Path(output_file_path), output) diff --git a/erdpy/projects/report/report_creator.py b/erdpy/projects/report/report_creator.py new file mode 100644 index 00000000..7bcc2467 --- /dev/null +++ b/erdpy/projects/report/report_creator.py @@ -0,0 +1,81 @@ + +import itertools +import operator +from pathlib import Path +from typing import Iterable, List, Tuple +from erdpy import guards +from erdpy.projects.report.data.folder_report import FolderReport +from erdpy.projects.report.data.option_results import OptionResults +from erdpy.projects.report.data.report import Report +from erdpy.projects.report.data.wasm_report import WasmReport +from erdpy.projects.report.data.project_report import ProjectReport +from erdpy.projects.core import load_project +from erdpy.projects.project_base import remove_suffix +from erdpy.projects.project_rust import ProjectRust + +from erdpy.projects.report.options.report_option import ReportOption +from erdpy.projects.report.options.twiggy_paths_check import run_twiggy_paths + + +class ReportCreator: + def __init__(self, options: List[ReportOption], skip_build: bool, skip_twiggy: bool) -> None: + self.options = options + self.skip_build = skip_build + self.skip_twiggy = skip_twiggy + self.require_twiggy_paths = any(option.requires_twiggy_paths() for option in self.options) + + + def apply_option(self, option: ReportOption, wasm_path: Path) -> OptionResults: + result = option.apply(wasm_path) + return OptionResults(option.name, [result]) + + + def create_wasm_report(self, wasm_path: Path, twiggy_requirements_met: bool) -> WasmReport: + if twiggy_requirements_met: + run_twiggy_paths(wasm_path) + name = wasm_path.name + option_results = [self.apply_option(option, wasm_path) for option in self.options] + return WasmReport(name, option_results) + + + def create_project_report(self, parent_path: Path, project_path: Path) -> ProjectReport: + project_path = project_path.resolve() + project = load_project(project_path) + + if not self.skip_build: + project.build() + + twiggy_requirements_met = False + should_build_twiggy = self.require_twiggy_paths and not self.skip_twiggy + if should_build_twiggy and isinstance(project, ProjectRust): + project.build_wasm_with_debug_symbols() + twiggy_requirements_met = True + + wasm_reports = [self.create_wasm_report(wasm_path, twiggy_requirements_met) for wasm_path in project.find_wasm_files()] + wasm_reports.sort(key=lambda report: remove_suffix(report.wasm_name, '.wasm')) + + return ProjectReport(project_path.relative_to(parent_path), wasm_reports) + + + def create_folder_report(self, base_path: Path, parent_folder: Path, iter: Iterable[Tuple[Path, Path]]) -> FolderReport: + parent_folder = parent_folder.resolve() + project_reports = [self.create_project_report(parent_folder, project_path) for _, project_path in iter] + + root_path = parent_folder.relative_to(base_path.parent) + return FolderReport(root_path, project_reports) + + + def create_report(self, base_path: Path, project_paths: List[Path]) -> Report: + base_path = base_path.resolve() + guards.is_directory(base_path) + + folder_groups = [self.create_folder_report(base_path, parent_folder, iter) + for parent_folder, iter in group_projects_by_folder(project_paths)] + + option_names = [option.name for option in self.options] + return Report(option_names, folder_groups) + + +def group_projects_by_folder(project_paths: List[Path]) -> Iterable[Tuple[Path, Iterable[Tuple[Path, Path]]]]: + path_pairs = sorted([(path.parent, path) for path in project_paths]) + return itertools.groupby(path_pairs, operator.itemgetter(0)) From 7757c8d1d6ded6276309afbee7c6a6acdbddd863 Mon Sep 17 00:00:00 2001 From: Claudiu-Marcel Bruda Date: Wed, 23 Feb 2022 10:41:47 +0200 Subject: [PATCH 26/44] display a single option result when unchanged --- erdpy/projects/report/data/option_results.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erdpy/projects/report/data/option_results.py b/erdpy/projects/report/data/option_results.py index bf467cb2..174b4203 100644 --- a/erdpy/projects/report/data/option_results.py +++ b/erdpy/projects/report/data/option_results.py @@ -19,7 +19,11 @@ def from_json(json: Any) -> 'OptionResults': return OptionResults(json['option_name'], json['results']) def results_to_markdown(self) -> str: - return ' -> '.join(self.results) + all_results_are_identical = all(result == self.results[0] for result in self.results) + if all_results_are_identical: + return self.results[0] + else: + return ' -> '.join(self.results) def merge_lists_of_option_results(first: List[OptionResults], second: List[OptionResults]) -> List[OptionResults]: From 844ed6c4ca9ad9a955df37546238f2e29ddd3ae0 Mon Sep 17 00:00:00 2001 From: Claudiu-Marcel Bruda Date: Wed, 23 Feb 2022 11:22:13 +0200 Subject: [PATCH 27/44] clean-up imports, small refactor --- erdpy/projects/report/data/report.py | 5 +---- erdpy/projects/report/options/twiggy_paths_check.py | 7 ++----- erdpy/projects/report/report_cli.py | 9 ++++++--- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/erdpy/projects/report/data/report.py b/erdpy/projects/report/data/report.py index 7693d68d..0d766665 100644 --- a/erdpy/projects/report/data/report.py +++ b/erdpy/projects/report/data/report.py @@ -5,10 +5,7 @@ from typing import Any, List from erdpy.projects.report.data.folder_report import FolderReport, merge_list_of_folder_reports -from erdpy.projects.report.data.common import flatten_list_of_rows, merge_values, merge_values_by_key -from erdpy.projects.report.data.option_results import OptionResults -from erdpy.projects.report.data.project_report import ProjectReport -from erdpy.projects.report.data.wasm_report import WasmReport +from erdpy.projects.report.data.common import flatten_list_of_rows, merge_values class Report: diff --git a/erdpy/projects/report/options/twiggy_paths_check.py b/erdpy/projects/report/options/twiggy_paths_check.py index af6a6840..3fdbf752 100644 --- a/erdpy/projects/report/options/twiggy_paths_check.py +++ b/erdpy/projects/report/options/twiggy_paths_check.py @@ -1,12 +1,9 @@ import logging from pathlib import Path + from erdpy import dependencies, myprocess, utils from erdpy.errors import BadFile - -from erdpy.projects.project_base import remove_suffix - -from .report_option import ReportOption - +from erdpy.projects.report.options.report_option import ReportOption logger = logging.getLogger("projects.report.options.twiggy_paths_check") diff --git a/erdpy/projects/report/report_cli.py b/erdpy/projects/report/report_cli.py index 1dedd680..974f3bd0 100644 --- a/erdpy/projects/report/report_cli.py +++ b/erdpy/projects/report/report_cli.py @@ -4,7 +4,7 @@ from erdpy import utils from erdpy.projects.core import get_project_paths_recursively -from erdpy.projects.report.data.report import Report, merge_list_of_reports, merge_two_reports +from erdpy.projects.report.data.report import Report, merge_list_of_reports from erdpy.projects.report.options.builder import get_default_report_options from erdpy.projects.report.report_creator import ReportCreator @@ -38,13 +38,16 @@ def finalize_report(report: Report, args: Any) -> None: output = get_report_output_string(report, args) store_output(output, args) + def get_report_output_string(report: Report, args: Any) -> str: - if args.output_format == "markdown": + output_format = args.output_format + if output_format == "markdown": return report.to_markdown() - elif args.output_format == "json": + elif output_format == "json": return report.to_json_string() raise Exception('Invalid output format') + def store_output(output: str, args: Any) -> None: output_file_path = args.output_file if output_file_path is None: From b7fc165d6624377863aa51d5b7d20a320785bba6 Mon Sep 17 00:00:00 2001 From: Claudiu-Marcel Bruda Date: Wed, 23 Feb 2022 11:45:30 +0200 Subject: [PATCH 28/44] adjust column widths --- erdpy/projects/report/data/report.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erdpy/projects/report/data/report.py b/erdpy/projects/report/data/report.py index 0d766665..789fe6c2 100644 --- a/erdpy/projects/report/data/report.py +++ b/erdpy/projects/report/data/report.py @@ -72,10 +72,10 @@ def merge_two_reports(first: Report, other: Report) -> Report: def format_row_markdown(row: List[str]) -> str: row += [''] * (4 - len(row)) - row[0] = row[0].ljust(80) - row[1] = row[1].rjust(15) - row[2] = row[2].rjust(15) - row[3] = row[3].rjust(15) + row[0] = row[0].ljust(100) + row[1] = row[1].rjust(20) + row[2] = row[2].rjust(20) + row[3] = row[3].rjust(20) merged_cells = " | ".join(row) return f"| {merged_cells} |" From 383e9712d212c7c51e56b100aca43edf8c1f25f3 Mon Sep 17 00:00:00 2001 From: Claudiu-Marcel Bruda Date: Wed, 23 Feb 2022 11:46:04 +0200 Subject: [PATCH 29/44] fix report comparison when folders are missing --- erdpy/projects/report/data/folder_report.py | 15 +++++++++++---- erdpy/projects/report/data/project_report.py | 15 +++++++++++---- erdpy/projects/report/data/wasm_report.py | 15 +++++++++++---- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/erdpy/projects/report/data/folder_report.py b/erdpy/projects/report/data/folder_report.py index 337840ce..c4586b74 100644 --- a/erdpy/projects/report/data/folder_report.py +++ b/erdpy/projects/report/data/folder_report.py @@ -34,8 +34,15 @@ def get_folder_report_root_path(item: FolderReport) -> Path: return item.root_path +def projects_or_default(folder_report: Optional[FolderReport]) -> List[ProjectReport]: + if folder_report is None: + return [] + return folder_report.projects + + def merge_two_folder_reports(first: Optional[FolderReport], second: Optional[FolderReport]) -> FolderReport: - if first is None or second is None: - return first_non_none(first, second) - merged_projects = merge_list_of_projects(first.projects, second.projects) - return FolderReport(first.root_path, merged_projects) + any = first_non_none(first, second) + first_projects = projects_or_default(first) + second_projects = projects_or_default(second) + merged_projects = merge_list_of_projects(first_projects, second_projects) + return FolderReport(any.root_path, merged_projects) diff --git a/erdpy/projects/report/data/project_report.py b/erdpy/projects/report/data/project_report.py index ec7e19c2..e7a697bc 100644 --- a/erdpy/projects/report/data/project_report.py +++ b/erdpy/projects/report/data/project_report.py @@ -41,8 +41,15 @@ def get_project_report_path(project_report: ProjectReport) -> Path: return project_report.project_path +def wasms_or_default(project_report: Optional[ProjectReport]) -> List[WasmReport]: + if project_report is None: + return [] + return project_report.wasms + + def merge_two_project_reports(first: Optional[ProjectReport], second: Optional[ProjectReport]) -> ProjectReport: - if first is None or second is None: - return first_non_none(first, second) - merged_wasms = merge_list_of_wasms(first.wasms, second.wasms) - return ProjectReport(first.project_path, merged_wasms) + any = first_non_none(first, second) + first_wasms = wasms_or_default(first) + second_wasms = wasms_or_default(second) + merged_wasms = merge_list_of_wasms(first_wasms, second_wasms) + return ProjectReport(any.project_path, merged_wasms) diff --git a/erdpy/projects/report/data/wasm_report.py b/erdpy/projects/report/data/wasm_report.py index b2713387..e0a80d0b 100644 --- a/erdpy/projects/report/data/wasm_report.py +++ b/erdpy/projects/report/data/wasm_report.py @@ -31,8 +31,15 @@ def get_wasm_key(wasm: WasmReport) -> str: return wasm.wasm_name +def get_option_results_or_default(wasm: Optional[WasmReport]) -> List[OptionResults]: + if wasm is None: + return [] + return wasm.option_results + + def merge_two_wasms(first: Optional[WasmReport], second: Optional[WasmReport]) -> WasmReport: - if first is None or second is None: - return first_non_none(first, second) - merged_option_results = merge_lists_of_option_results(first.option_results, second.option_results) - return WasmReport(first.wasm_name, merged_option_results) + any = first_non_none(first, second) + first_option_results = get_option_results_or_default(first) + second_option_results = get_option_results_or_default(second) + merged_option_results = merge_lists_of_option_results(first_option_results, second_option_results) + return WasmReport(any.wasm_name, merged_option_results) From 746cef08c171e4e19f761b0570b7c7c9575734ef Mon Sep 17 00:00:00 2001 From: Claudiu-Marcel Bruda Date: Wed, 23 Feb 2022 15:47:24 +0200 Subject: [PATCH 30/44] improve mardown formatting --- erdpy/cli_contracts.py | 2 +- erdpy/projects/report/data/folder_report.py | 10 ++- erdpy/projects/report/data/option_results.py | 63 +++++++++++++++++-- erdpy/projects/report/data/project_report.py | 12 ++-- erdpy/projects/report/data/report.py | 56 ++++++++++++----- erdpy/projects/report/data/wasm_report.py | 5 +- erdpy/projects/report/format/__init__.py | 0 erdpy/projects/report/format/change_type.py | 37 +++++++++++ .../projects/report/format/format_options.py | 3 + erdpy/projects/report/report_cli.py | 9 ++- 10 files changed, 164 insertions(+), 33 deletions(-) create mode 100644 erdpy/projects/report/format/__init__.py create mode 100644 erdpy/projects/report/format/change_type.py create mode 100644 erdpy/projects/report/format/format_options.py diff --git a/erdpy/cli_contracts.py b/erdpy/cli_contracts.py index f870d0aa..5970f2c3 100644 --- a/erdpy/cli_contracts.py +++ b/erdpy/cli_contracts.py @@ -64,7 +64,7 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: _add_project_arg(sub) _add_flag(sub, "--skip-build", help="skips the step of building of the wasm contracts") _add_flag(sub, "--skip-twiggy", help="skips the steps of building the debug wasm files and running twiggy") - sub.add_argument("--output-format", type=str, default="markdown", choices=["markdown", "json"], help="report output format (default: %(default)s)") + sub.add_argument("--output-format", type=str, default="text-markdown", choices=["github-markdown", "text-markdown", "json"], help="report output format (default: %(default)s)") sub.add_argument("--output-file", type=Path, help="if specified, the output is written to a file, otherwise it's written to the standard output") sub.add_argument("--compare", type=Path, nargs='+', metavar=("report-1.json", "report-2.json"), help="create a comparison from two or more reports") sub.set_defaults(func=projects.report_cli) diff --git a/erdpy/projects/report/data/folder_report.py b/erdpy/projects/report/data/folder_report.py index c4586b74..51290732 100644 --- a/erdpy/projects/report/data/folder_report.py +++ b/erdpy/projects/report/data/folder_report.py @@ -3,6 +3,7 @@ from erdpy.projects.report.data.common import first_non_none, flatten_list_of_rows, merge_values_by_key from erdpy.projects.report.data.project_report import ProjectReport, merge_list_of_projects +from erdpy.projects.report.format.format_options import FormatOptions class FolderReport: @@ -10,19 +11,22 @@ def __init__(self, root_path: Path, projects: List[ProjectReport]) -> None: self.root_path = root_path self.projects = projects + def to_json(self) -> Any: return { 'root_path': str(self.root_path), 'projects': self.projects } + def from_json(json: Any) -> 'FolderReport': projects = [ProjectReport.from_json(project) for project in json['projects']] return FolderReport(Path(json['root_path']), projects) - - def get_rows(self) -> List[List[str]]: + + + def get_markdown_rows(self, format_options: FormatOptions) -> List[List[str]]: folder_row = [str(self.root_path)] - project_rows = flatten_list_of_rows([project.get_rows_markdown() for project in self.projects]) + project_rows = flatten_list_of_rows([project.get_rows_markdown(format_options) for project in self.projects]) return [folder_row] + project_rows diff --git a/erdpy/projects/report/data/option_results.py b/erdpy/projects/report/data/option_results.py index 174b4203..aac4cb3a 100644 --- a/erdpy/projects/report/data/option_results.py +++ b/erdpy/projects/report/data/option_results.py @@ -1,6 +1,8 @@ from typing import Any, List, Optional from erdpy.projects.report.data.common import first_non_none, merge_values_by_key +from erdpy.projects.report.format.change_type import ChangeType +from erdpy.projects.report.format.format_options import FormatOptions class OptionResults: @@ -8,22 +10,73 @@ def __init__(self, option_name: str, results: List[str]) -> None: self.option_name = option_name self.results = results + def to_json(self) -> Any: return { 'option_name': self.option_name, 'results': self.results } - + + @staticmethod def from_json(json: Any) -> 'OptionResults': return OptionResults(json['option_name'], json['results']) - def results_to_markdown(self) -> str: + + def results_to_markdown(self, format_options: FormatOptions) -> str: + separator = ' :arrow_right: ' if format_options.github_flavor else ' -> ' + change_type = self.classify_changes() + display_results = prepare_results_for_markdown(self.results) + if change_type == ChangeType.NONE: + return display_results[0] + else: + return separator.join(display_results) + ' ' + change_type.to_markdown(format_options) + + + def classify_changes(self) -> ChangeType: all_results_are_identical = all(result == self.results[0] for result in self.results) if all_results_are_identical: - return self.results[0] - else: - return ' -> '.join(self.results) + return ChangeType.NONE + any_is_not_available = any(result == 'N/A' for result in self.results) + if any_is_not_available: + return ChangeType.UNKNOWN + if self.option_name == 'size': + sizes = list(map(int, self.results)) + return classify_list(sizes, reverse=True) + if self.option_name == 'has-allocator' or self.option_name == 'has-format': + presence_checks = list(map(lambda value: False if value == 'False' else True, self.results)) + return classify_list(presence_checks, reverse=True) + return ChangeType.UNKNOWN + + +def prepare_results_for_markdown(results: List[str]) -> List[str]: + return list(map(replace_bool_with_yes_no, results)) + + +def replace_bool_with_yes_no(item: str) -> str: + if item == 'True': + return 'Yes' + if item == 'False': + return 'No' + return item + + +def classify_list(items: List[Any], reverse: bool = False) -> ChangeType: + if reverse: + items.reverse() + if is_strictly_better(items): + return ChangeType.GOOD + if is_strictly_worse(items): + return ChangeType.BAD + return ChangeType.MIXED + + +def is_strictly_better(items: List[Any]) -> bool: + return sorted(items) == items + + +def is_strictly_worse(items: List[Any]) -> bool: + return sorted(items, reverse=True) == items def merge_lists_of_option_results(first: List[OptionResults], second: List[OptionResults]) -> List[OptionResults]: diff --git a/erdpy/projects/report/data/project_report.py b/erdpy/projects/report/data/project_report.py index e7a697bc..7e5c7460 100644 --- a/erdpy/projects/report/data/project_report.py +++ b/erdpy/projects/report/data/project_report.py @@ -3,33 +3,37 @@ from erdpy.projects.report.data.common import first_non_none, merge_values_by_key from erdpy.projects.report.data.wasm_report import WasmReport, merge_list_of_wasms +from erdpy.projects.report.format.format_options import FormatOptions class ProjectReport: def __init__(self, project_path: Path, wasms: List[WasmReport]) -> None: self.project_path = project_path self.wasms = wasms - + + def to_json(self) -> Any: return { "project_path": str(self.project_path), 'wasms': self.wasms } + @staticmethod def from_json(json: Any) -> 'ProjectReport': wasms = [WasmReport.from_json(wasm) for wasm in json['wasms']] return ProjectReport(Path(json['project_path']), wasms) - def get_rows_markdown(self) -> List[List[str]]: + + def get_rows_markdown(self, format_options: FormatOptions) -> List[List[str]]: wasm_count = len(self.wasms) if wasm_count == 0: return [[f" - {str(self.project_path)} "]] elif wasm_count == 1: - return [[f" - {str(self.project_path / self.wasms[0].wasm_name)}"] + self.wasms[0].get_option_results()] + return [[f" - {str(self.project_path / self.wasms[0].wasm_name)}"] + self.wasms[0].get_option_results(format_options)] else: project_path_row = [f" - {str(self.project_path)}"] - wasm_rows = [[f" - - {wasm.wasm_name}"] + wasm.get_option_results() for wasm in self.wasms] + wasm_rows = [[f" - - {wasm.wasm_name}"] + wasm.get_option_results(format_options) for wasm in self.wasms] return [project_path_row] + wasm_rows diff --git a/erdpy/projects/report/data/report.py b/erdpy/projects/report/data/report.py index 789fe6c2..f1cb62d0 100644 --- a/erdpy/projects/report/data/report.py +++ b/erdpy/projects/report/data/report.py @@ -6,6 +6,8 @@ from erdpy.projects.report.data.folder_report import FolderReport, merge_list_of_folder_reports from erdpy.projects.report.data.common import flatten_list_of_rows, merge_values +from erdpy.projects.report.format.change_type import ChangeType +from erdpy.projects.report.format.format_options import FormatOptions class Report: @@ -34,24 +36,25 @@ def load_from_file(report_json_path: Path) -> 'Report': return Report.from_json(report_json) - def get_rows(self) -> List[List[str]]: - rows = [group.get_rows() for group in self.folders] + def get_markdown_rows(self, format_options: FormatOptions) -> List[List[str]]: + rows = [folder_report.get_markdown_rows(format_options) for folder_report in self.folders] return flatten_list_of_rows(rows) - def to_markdown(self) -> str: + def to_markdown(self, format_options: FormatOptions) -> str: string = StringIO() - table_header = ["Path"] + self.option_names - write_markdown_row(string, table_header) + table_headers = ["Path"] + self.option_names + adjust_table_headers(table_headers, format_options) + write_markdown_row(string, table_headers, format_options) ALIGN_LEFT = ":--" ALIGN_RIGHT = "--:" row_alignments = [ALIGN_LEFT] + len(self.option_names) * [ALIGN_RIGHT] - write_markdown_row(string, row_alignments) + write_markdown_row(string, row_alignments, format_options) - for row in self.get_rows(): - write_markdown_row(string, row) + for row in self.get_markdown_rows(format_options): + write_markdown_row(string, row, format_options) return string.getvalue() @@ -60,6 +63,18 @@ def to_json_string(self) -> str: return json.dumps(self, indent=4, default=lambda obj: obj.to_json()) +# In order to fix column widths - see: +# https://github.com/markedjs/marked/issues/266#issuecomment-616347986 +def adjust_table_headers(table_headers: List[str], format_options: FormatOptions) -> None: + if not format_options.github_flavor: + return + NBSP = '\u00A0' + table_headers[0] = table_headers[0].ljust(60, NBSP) + table_headers[1] = table_headers[1].rjust(40, NBSP) + table_headers[2] = table_headers[2].rjust(30, NBSP) + table_headers[3] = table_headers[3].rjust(30, NBSP) + + def merge_list_of_reports(reports: List[Report]) -> Report: return functools.reduce(merge_two_reports, reports) @@ -70,16 +85,27 @@ def merge_two_reports(first: Report, other: Report) -> Report: return Report(option_names, folders) -def format_row_markdown(row: List[str]) -> str: +def justify_text_string(string: str, width: int) -> str: + if ChangeType.UNKNOWN.to_text_markdown() in string: + width += 1 + if ChangeType.GOOD.to_text_markdown() in string: + width -= 1 + if ChangeType.BAD.to_text_markdown() in string: + width -= 1 + return string.rjust(width) + + +def format_row_markdown(row: List[str], format_options: FormatOptions) -> str: row += [''] * (4 - len(row)) - row[0] = row[0].ljust(100) - row[1] = row[1].rjust(20) - row[2] = row[2].rjust(20) - row[3] = row[3].rjust(20) + if not format_options.github_flavor: + row[0] = row[0].ljust(100) + row[1] = justify_text_string(row[1], 20) + row[2] = justify_text_string(row[2], 20) + row[3] = justify_text_string(row[3], 20) merged_cells = " | ".join(row) return f"| {merged_cells} |" -def write_markdown_row(string: StringIO, row: List[str]): - string.write(format_row_markdown(row)) +def write_markdown_row(string: StringIO, row: List[str], format_options: FormatOptions): + string.write(format_row_markdown(row, format_options)) string.write('\n') diff --git a/erdpy/projects/report/data/wasm_report.py b/erdpy/projects/report/data/wasm_report.py index e0a80d0b..990deaa2 100644 --- a/erdpy/projects/report/data/wasm_report.py +++ b/erdpy/projects/report/data/wasm_report.py @@ -1,6 +1,7 @@ from typing import Any, List, Optional from erdpy.projects.report.data.common import first_non_none, merge_values_by_key from erdpy.projects.report.data.option_results import OptionResults, merge_lists_of_option_results +from erdpy.projects.report.format.format_options import FormatOptions class WasmReport: @@ -19,8 +20,8 @@ def from_json(json: Any) -> 'WasmReport': option_results = [OptionResults.from_json(option_result) for option_result in json['option_results']] return WasmReport(json['wasm_name'], option_results) - def get_option_results(self) -> List[str]: - return [option.results_to_markdown() for option in self.option_results] + def get_option_results(self, format_options: FormatOptions) -> List[str]: + return [option.results_to_markdown(format_options) for option in self.option_results] def merge_list_of_wasms(first: List[WasmReport], second: List[WasmReport]) -> List[WasmReport]: diff --git a/erdpy/projects/report/format/__init__.py b/erdpy/projects/report/format/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/erdpy/projects/report/format/change_type.py b/erdpy/projects/report/format/change_type.py new file mode 100644 index 00000000..59f098c1 --- /dev/null +++ b/erdpy/projects/report/format/change_type.py @@ -0,0 +1,37 @@ +from enum import Enum, auto + +from erdpy.projects.report.format.format_options import FormatOptions + + +class ChangeType(Enum): + UNKNOWN = auto() + NONE = auto() + GOOD = auto() + BAD = auto() + MIXED = auto() + + def to_markdown(self, format_options: FormatOptions) -> str: + if format_options.github_flavor: + return self.to_github_markdown() + else: + return self.to_text_markdown() + + def to_github_markdown(self) -> str: + switch = { + ChangeType.UNKNOWN: ':warning:', + ChangeType.NONE: '', + ChangeType.GOOD: ':green_circle:', + ChangeType.BAD: ':red_circle:', + ChangeType.MIXED: ':yellow_circle:' + } + return switch[self] + + def to_text_markdown(self) -> str: + switch = { + ChangeType.UNKNOWN: '\u26a0\ufe0f ', + ChangeType.NONE: '', + ChangeType.GOOD: '\U0001F34F', + ChangeType.BAD: '\u274C', + ChangeType.MIXED: '\U0001F536\uFE0F' + } + return switch[self] diff --git a/erdpy/projects/report/format/format_options.py b/erdpy/projects/report/format/format_options.py new file mode 100644 index 00000000..ca2a068c --- /dev/null +++ b/erdpy/projects/report/format/format_options.py @@ -0,0 +1,3 @@ +class FormatOptions: + def __init__(self, github_markdown: bool) -> None: + self.github_flavor = github_markdown diff --git a/erdpy/projects/report/report_cli.py b/erdpy/projects/report/report_cli.py index 974f3bd0..e4389c11 100644 --- a/erdpy/projects/report/report_cli.py +++ b/erdpy/projects/report/report_cli.py @@ -5,6 +5,7 @@ from erdpy.projects.core import get_project_paths_recursively from erdpy.projects.report.data.report import Report, merge_list_of_reports +from erdpy.projects.report.format.format_options import FormatOptions from erdpy.projects.report.options.builder import get_default_report_options from erdpy.projects.report.report_creator import ReportCreator @@ -41,9 +42,11 @@ def finalize_report(report: Report, args: Any) -> None: def get_report_output_string(report: Report, args: Any) -> str: output_format = args.output_format - if output_format == "markdown": - return report.to_markdown() - elif output_format == "json": + if output_format == 'github-markdown': + return report.to_markdown(FormatOptions(github_markdown=True)) + if output_format == 'text-markdown': + return report.to_markdown(FormatOptions(github_markdown=False)) + elif output_format == 'json': return report.to_json_string() raise Exception('Invalid output format') From de92c405991344e5b6d44f6d3f541243cb68dd12 Mon Sep 17 00:00:00 2001 From: Claudiu-Marcel Bruda Date: Wed, 23 Feb 2022 16:43:50 +0200 Subject: [PATCH 31/44] add comments --- erdpy/projects/report/data/report.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erdpy/projects/report/data/report.py b/erdpy/projects/report/data/report.py index f1cb62d0..b038190f 100644 --- a/erdpy/projects/report/data/report.py +++ b/erdpy/projects/report/data/report.py @@ -63,7 +63,7 @@ def to_json_string(self) -> str: return json.dumps(self, indent=4, default=lambda obj: obj.to_json()) -# In order to fix column widths - see: +# Adjusts the column widths in github tables - see: # https://github.com/markedjs/marked/issues/266#issuecomment-616347986 def adjust_table_headers(table_headers: List[str], format_options: FormatOptions) -> None: if not format_options.github_flavor: @@ -85,6 +85,8 @@ def merge_two_reports(first: Report, other: Report) -> Report: return Report(option_names, folders) +# Hack in order to keep the column alignment in a terminal +# as unicode characters are sometimes wider or narrower def justify_text_string(string: str, width: int) -> str: if ChangeType.UNKNOWN.to_text_markdown() in string: width += 1 From 9a618d4f6b8e524b174b759abd9815cfc542a762 Mon Sep 17 00:00:00 2001 From: Claudiu-Marcel Bruda Date: Fri, 25 Feb 2022 14:27:21 +0200 Subject: [PATCH 32/44] typo fix --- erdpy/cli_contracts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erdpy/cli_contracts.py b/erdpy/cli_contracts.py index 5970f2c3..90f48752 100644 --- a/erdpy/cli_contracts.py +++ b/erdpy/cli_contracts.py @@ -60,7 +60,7 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: sub.add_argument("--wildcard", required=False, help="wildcard to match only specific test files") sub.set_defaults(func=run_tests) - sub = cli_shared.add_command_subparser(subparsers, "contract", "report", "Print a detailed report the smart contracts.") + sub = cli_shared.add_command_subparser(subparsers, "contract", "report", "Print a detailed report of the smart contracts.") _add_project_arg(sub) _add_flag(sub, "--skip-build", help="skips the step of building of the wasm contracts") _add_flag(sub, "--skip-twiggy", help="skips the steps of building the debug wasm files and running twiggy") From 9ede79764c9a6fcceea416f277fe9f15ca09b49f Mon Sep 17 00:00:00 2001 From: Claudiu-Marcel Bruda Date: Fri, 25 Feb 2022 14:31:01 +0200 Subject: [PATCH 33/44] inline _add_flag --- erdpy/cli_contracts.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/erdpy/cli_contracts.py b/erdpy/cli_contracts.py index 90f48752..6ffcb531 100644 --- a/erdpy/cli_contracts.py +++ b/erdpy/cli_contracts.py @@ -62,8 +62,8 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: sub = cli_shared.add_command_subparser(subparsers, "contract", "report", "Print a detailed report of the smart contracts.") _add_project_arg(sub) - _add_flag(sub, "--skip-build", help="skips the step of building of the wasm contracts") - _add_flag(sub, "--skip-twiggy", help="skips the steps of building the debug wasm files and running twiggy") + sub.add_argument("--skip-build", action="store_true", default=False, help="skips the step of building of the wasm contracts") + sub.add_argument("--skip-twiggy", action="store_true", default=False, help="skips the steps of building the debug wasm files and running twiggy") sub.add_argument("--output-format", type=str, default="text-markdown", choices=["github-markdown", "text-markdown", "json"], help="report output format (default: %(default)s)") sub.add_argument("--output-file", type=Path, help="if specified, the output is written to a file, otherwise it's written to the standard output") sub.add_argument("--compare", type=Path, nargs='+', metavar=("report-1.json", "report-2.json"), help="create a comparison from two or more reports") @@ -142,10 +142,6 @@ def _add_recursive_arg(sub: Any): sub.add_argument("-r", "--recursive", dest="recursive", action="store_true", help="locate projects recursively") -def _add_flag(sub: Any, flag: str, help: str): - sub.add_argument(flag, action="store_true", default=False, help=help) - - def _add_project_or_bytecode_arg(sub: Any): group = sub.add_mutually_exclusive_group(required=True) group.add_argument("--project", default=os.getcwd(), From e39b3eb8c099fb2f1841c25454642478a38c4075 Mon Sep 17 00:00:00 2001 From: Claudiu-Marcel Bruda Date: Fri, 25 Feb 2022 14:31:29 +0200 Subject: [PATCH 34/44] move run_command_with_rust_env and make it private --- erdpy/dependencies/modules.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erdpy/dependencies/modules.py b/erdpy/dependencies/modules.py index 4f0ada2c..b0a9949e 100644 --- a/erdpy/dependencies/modules.py +++ b/erdpy/dependencies/modules.py @@ -340,12 +340,8 @@ def __init__(self, key: str, aliases: List[str] = None): super().__init__(key, aliases) - def run_command_with_rust_env(self, args: List[str]) -> str: - rust = dependencies.get_module_by_key("rust") - return myprocess.run_process(args, rust.get_env()) - def _do_install(self, tag: str) -> None: - self.run_command_with_rust_env(["cargo", "install", self.key]) + self._run_command_with_rust_env(["cargo", "install", self.key]) def is_installed(self, tag: str) -> bool: rust = dependencies.get_module_by_key("rust") @@ -357,11 +353,15 @@ def is_installed(self, tag: str) -> bool: def uninstall(self, tag: str): if self.is_installed(tag): - self.run_command_with_rust_env(["cargo", "uninstall", self.key]) + self._run_command_with_rust_env(["cargo", "uninstall", self.key]) def get_latest_release(self) -> str: return "latest" + def _run_command_with_rust_env(self, args: List[str]) -> str: + rust = dependencies.get_module_by_key("rust") + return myprocess.run_process(args, rust.get_env()) + class MclSignerModule(StandaloneModule): def __init__(self, key: str, aliases: List[str] = None): From 3cb16e3cbe7a887a79a5fc3e69906e34e592283a Mon Sep 17 00:00:00 2001 From: Claudiu-Marcel Bruda Date: Fri, 25 Feb 2022 18:36:16 +0200 Subject: [PATCH 35/44] fixes after review --- erdpy/dependencies/modules.py | 5 +- erdpy/projects/__init__.py | 2 +- erdpy/projects/report/__init__.py | 2 + erdpy/projects/report/data/common.py | 2 +- erdpy/projects/report/data/folder_report.py | 7 +- erdpy/projects/report/data/option_results.py | 78 ++++++++++--------- erdpy/projects/report/data/project_report.py | 7 +- erdpy/projects/report/data/report.py | 16 ++-- erdpy/projects/report/data/wasm_report.py | 14 ++-- .../report/{options => features}/__init__.py | 0 erdpy/projects/report/features/features.py | 13 ++++ .../{options => features}/report_option.py | 12 ++- .../report/{options => features}/size.py | 6 +- .../twiggy_paths_check.py | 47 ++++++----- erdpy/projects/report/options/builder.py | 11 --- erdpy/projects/report/report_cli.py | 5 +- erdpy/projects/report/report_creator.py | 58 +++++++------- 17 files changed, 143 insertions(+), 142 deletions(-) rename erdpy/projects/report/{options => features}/__init__.py (100%) create mode 100644 erdpy/projects/report/features/features.py rename erdpy/projects/report/{options => features}/report_option.py (53%) rename erdpy/projects/report/{options => features}/size.py (70%) rename erdpy/projects/report/{options => features}/twiggy_paths_check.py (61%) delete mode 100644 erdpy/projects/report/options/builder.py diff --git a/erdpy/dependencies/modules.py b/erdpy/dependencies/modules.py index b0a9949e..e837d834 100644 --- a/erdpy/dependencies/modules.py +++ b/erdpy/dependencies/modules.py @@ -251,7 +251,7 @@ def __init__(self, key: str, aliases: List[str] = []): def get_nodejs(self) -> DependencyModule: return dependencies.get_module_by_key("nodejs") - + def get_nodejs_env(self) -> Dict[str, str]: return self.get_nodejs().get_env() @@ -276,10 +276,11 @@ def is_installed(self, tag: str) -> bool: return True except FileNotFoundError: return False - + def get_latest_release(self) -> str: return "latest" + class Rust(DependencyModule): def __init__(self, key: str, aliases: List[str] = None): if aliases is None: diff --git a/erdpy/projects/__init__.py b/erdpy/projects/__init__.py index a8e1af69..2bdf7dad 100644 --- a/erdpy/projects/__init__.py +++ b/erdpy/projects/__init__.py @@ -10,4 +10,4 @@ from erdpy.projects.templates import (create_from_template, list_project_templates) -__all__ = ["build_project", "clean_project", "build_report_options", "report_cli", "run_tests", "get_projects_in_workspace", "load_project", "Project", "ProjectClang", "ProjectCpp", "ProjectRust", "ProjectSol", "create_from_template", "list_project_templates"] +__all__ = ["build_project", "clean_project", "report_cli", "run_tests", "get_projects_in_workspace", "load_project", "Project", "ProjectClang", "ProjectCpp", "ProjectRust", "ProjectSol", "create_from_template", "list_project_templates"] diff --git a/erdpy/projects/report/__init__.py b/erdpy/projects/report/__init__.py index 2f67cd2a..35cd7aa1 100644 --- a/erdpy/projects/report/__init__.py +++ b/erdpy/projects/report/__init__.py @@ -1 +1,3 @@ from .report_cli import report_cli + +__all__ = ["report_cli"] diff --git a/erdpy/projects/report/data/common.py b/erdpy/projects/report/data/common.py index f8ce6511..afd5fe67 100644 --- a/erdpy/projects/report/data/common.py +++ b/erdpy/projects/report/data/common.py @@ -15,7 +15,7 @@ def merge_values(first: List[str], second: List[str]) -> List[str]: K = TypeVar('K') -def first_non_none(first: Optional[T], second: Optional[T]) -> T: +def first_not_none(first: Optional[T], second: Optional[T]) -> T: return next(item for item in [first, second] if item is not None) diff --git a/erdpy/projects/report/data/folder_report.py b/erdpy/projects/report/data/folder_report.py index 51290732..bc54de2c 100644 --- a/erdpy/projects/report/data/folder_report.py +++ b/erdpy/projects/report/data/folder_report.py @@ -1,6 +1,6 @@ from pathlib import Path from typing import Any, List, Optional -from erdpy.projects.report.data.common import first_non_none, flatten_list_of_rows, merge_values_by_key +from erdpy.projects.report.data.common import first_not_none, flatten_list_of_rows, merge_values_by_key from erdpy.projects.report.data.project_report import ProjectReport, merge_list_of_projects from erdpy.projects.report.format.format_options import FormatOptions @@ -11,19 +11,16 @@ def __init__(self, root_path: Path, projects: List[ProjectReport]) -> None: self.root_path = root_path self.projects = projects - def to_json(self) -> Any: return { 'root_path': str(self.root_path), 'projects': self.projects } - def from_json(json: Any) -> 'FolderReport': projects = [ProjectReport.from_json(project) for project in json['projects']] return FolderReport(Path(json['root_path']), projects) - def get_markdown_rows(self, format_options: FormatOptions) -> List[List[str]]: folder_row = [str(self.root_path)] project_rows = flatten_list_of_rows([project.get_rows_markdown(format_options) for project in self.projects]) @@ -45,7 +42,7 @@ def projects_or_default(folder_report: Optional[FolderReport]) -> List[ProjectRe def merge_two_folder_reports(first: Optional[FolderReport], second: Optional[FolderReport]) -> FolderReport: - any = first_non_none(first, second) + any = first_not_none(first, second) first_projects = projects_or_default(first) second_projects = projects_or_default(second) merged_projects = merge_list_of_projects(first_projects, second_projects) diff --git a/erdpy/projects/report/data/option_results.py b/erdpy/projects/report/data/option_results.py index aac4cb3a..bf035344 100644 --- a/erdpy/projects/report/data/option_results.py +++ b/erdpy/projects/report/data/option_results.py @@ -1,59 +1,63 @@ from typing import Any, List, Optional -from erdpy.projects.report.data.common import first_non_none, merge_values_by_key +from erdpy.projects.report.data.common import first_not_none, merge_values_by_key from erdpy.projects.report.format.change_type import ChangeType from erdpy.projects.report.format.format_options import FormatOptions -class OptionResults: - def __init__(self, option_name: str, results: List[str]) -> None: - self.option_name = option_name +class ExtractedFeature: + def __init__(self, feature_name: str, results: List[str]) -> None: + self.feature_name = feature_name self.results = results - def to_json(self) -> Any: return { - 'option_name': self.option_name, + 'feature_name': self.feature_name, 'results': self.results } - @staticmethod - def from_json(json: Any) -> 'OptionResults': - return OptionResults(json['option_name'], json['results']) - + def from_json(json: Any) -> 'ExtractedFeature': + return ExtractedFeature(json['feature_name'], json['results']) def results_to_markdown(self, format_options: FormatOptions) -> str: separator = ' :arrow_right: ' if format_options.github_flavor else ' -> ' - change_type = self.classify_changes() - display_results = prepare_results_for_markdown(self.results) + change_type = self._classify_changes() + display_results = _prepare_results_for_markdown(self.results) if change_type == ChangeType.NONE: return display_results[0] else: return separator.join(display_results) + ' ' + change_type.to_markdown(format_options) - - def classify_changes(self) -> ChangeType: + def _classify_changes(self) -> ChangeType: + """ + Used to classify if a change in a feature's results is good, bad etc. + Required because changes in values are not always intuitive: + - a higher value may be worse than a small one (eg. for file sizes) + - a 'Yes' may be worse than a 'No' + This information is used to draw small icons at the end of a cell containing a change. + """ + all_results_are_identical = all(result == self.results[0] for result in self.results) if all_results_are_identical: return ChangeType.NONE any_is_not_available = any(result == 'N/A' for result in self.results) if any_is_not_available: return ChangeType.UNKNOWN - if self.option_name == 'size': + if self.feature_name == 'size': sizes = list(map(int, self.results)) - return classify_list(sizes, reverse=True) - if self.option_name == 'has-allocator' or self.option_name == 'has-format': + return _classify_list(sizes, reverse=True) + if self.feature_name == 'has-allocator' or self.feature_name == 'has-format': presence_checks = list(map(lambda value: False if value == 'False' else True, self.results)) - return classify_list(presence_checks, reverse=True) + return _classify_list(presence_checks, reverse=True) return ChangeType.UNKNOWN -def prepare_results_for_markdown(results: List[str]) -> List[str]: - return list(map(replace_bool_with_yes_no, results)) +def _prepare_results_for_markdown(results: List[str]) -> List[str]: + return list(map(_replace_bool_with_yes_no, results)) -def replace_bool_with_yes_no(item: str) -> str: +def _replace_bool_with_yes_no(item: str) -> str: if item == 'True': return 'Yes' if item == 'False': @@ -61,40 +65,40 @@ def replace_bool_with_yes_no(item: str) -> str: return item -def classify_list(items: List[Any], reverse: bool = False) -> ChangeType: +def _classify_list(items: List[Any], reverse: bool = False) -> ChangeType: if reverse: items.reverse() - if is_strictly_better(items): + if _is_strictly_better(items): return ChangeType.GOOD - if is_strictly_worse(items): + if _is_strictly_worse(items): return ChangeType.BAD return ChangeType.MIXED -def is_strictly_better(items: List[Any]) -> bool: +def _is_strictly_better(items: List[Any]) -> bool: return sorted(items) == items -def is_strictly_worse(items: List[Any]) -> bool: +def _is_strictly_worse(items: List[Any]) -> bool: return sorted(items, reverse=True) == items -def merge_lists_of_option_results(first: List[OptionResults], second: List[OptionResults]) -> List[OptionResults]: - return merge_values_by_key(first, second, get_option_result_key, merge_two_option_results) +def merge_lists_of_option_results(first: List[ExtractedFeature], second: List[ExtractedFeature]) -> List[ExtractedFeature]: + return merge_values_by_key(first, second, _get_option_result_key, _merge_two_option_results) -def get_option_result_key(option_results: OptionResults) -> str: - return option_results.option_name +def _get_option_result_key(option_results: ExtractedFeature) -> str: + return option_results.feature_name -def merge_two_option_results(first: Optional[OptionResults], second: Optional[OptionResults]) -> OptionResults: - any = first_non_none(first, second) - merged_results = results_or_NA(first) + results_or_NA(second) - return OptionResults(any.option_name, merged_results) +def _merge_two_option_results(first: Optional[ExtractedFeature], second: Optional[ExtractedFeature]) -> ExtractedFeature: + any = first_not_none(first, second) + merged_results = _results_or_NA(first) + _results_or_NA(second) + return ExtractedFeature(any.feature_name, merged_results) -def results_or_NA(option_result: Optional[OptionResults]) -> List[str]: - if option_result is None: +def _results_or_NA(extracted_feature: Optional[ExtractedFeature]) -> List[str]: + if extracted_feature is None: return ['N/A'] else: - return option_result.results + return extracted_feature.results diff --git a/erdpy/projects/report/data/project_report.py b/erdpy/projects/report/data/project_report.py index 7e5c7460..ed291184 100644 --- a/erdpy/projects/report/data/project_report.py +++ b/erdpy/projects/report/data/project_report.py @@ -1,6 +1,6 @@ from pathlib import Path from typing import Any, List, Optional -from erdpy.projects.report.data.common import first_non_none, merge_values_by_key +from erdpy.projects.report.data.common import first_not_none, merge_values_by_key from erdpy.projects.report.data.wasm_report import WasmReport, merge_list_of_wasms from erdpy.projects.report.format.format_options import FormatOptions @@ -11,20 +11,17 @@ def __init__(self, project_path: Path, wasms: List[WasmReport]) -> None: self.project_path = project_path self.wasms = wasms - def to_json(self) -> Any: return { "project_path": str(self.project_path), 'wasms': self.wasms } - @staticmethod def from_json(json: Any) -> 'ProjectReport': wasms = [WasmReport.from_json(wasm) for wasm in json['wasms']] return ProjectReport(Path(json['project_path']), wasms) - def get_rows_markdown(self, format_options: FormatOptions) -> List[List[str]]: wasm_count = len(self.wasms) if wasm_count == 0: @@ -52,7 +49,7 @@ def wasms_or_default(project_report: Optional[ProjectReport]) -> List[WasmReport def merge_two_project_reports(first: Optional[ProjectReport], second: Optional[ProjectReport]) -> ProjectReport: - any = first_non_none(first, second) + any = first_not_none(first, second) first_wasms = wasms_or_default(first) second_wasms = wasms_or_default(second) merged_wasms = merge_list_of_wasms(first_wasms, second_wasms) diff --git a/erdpy/projects/report/data/report.py b/erdpy/projects/report/data/report.py index b038190f..1d5c784a 100644 --- a/erdpy/projects/report/data/report.py +++ b/erdpy/projects/report/data/report.py @@ -14,50 +14,44 @@ class Report: def __init__(self, option_names: List[str], folders: List[FolderReport]) -> None: self.option_names = option_names self.folders = folders - def to_json(self) -> Any: return { 'options': self.option_names, 'folders': self.folders } - @staticmethod def from_json(json: Any) -> 'Report': folders = [FolderReport.from_json(folder_report) for folder_report in json['folders']] return Report(json['options'], folders) - @staticmethod def load_from_file(report_json_path: Path) -> 'Report': with open(report_json_path, 'r') as report_file: report_json = json.load(report_file) return Report.from_json(report_json) - def get_markdown_rows(self, format_options: FormatOptions) -> List[List[str]]: rows = [folder_report.get_markdown_rows(format_options) for folder_report in self.folders] return flatten_list_of_rows(rows) - def to_markdown(self, format_options: FormatOptions) -> str: - string = StringIO() + text = StringIO() table_headers = ["Path"] + self.option_names adjust_table_headers(table_headers, format_options) - write_markdown_row(string, table_headers, format_options) + write_markdown_row(text, table_headers, format_options) ALIGN_LEFT = ":--" ALIGN_RIGHT = "--:" row_alignments = [ALIGN_LEFT] + len(self.option_names) * [ALIGN_RIGHT] - write_markdown_row(string, row_alignments, format_options) + write_markdown_row(text, row_alignments, format_options) for row in self.get_markdown_rows(format_options): - write_markdown_row(string, row, format_options) - - return string.getvalue() + write_markdown_row(text, row, format_options) + return text.getvalue() def to_json_string(self) -> str: return json.dumps(self, indent=4, default=lambda obj: obj.to_json()) diff --git a/erdpy/projects/report/data/wasm_report.py b/erdpy/projects/report/data/wasm_report.py index 990deaa2..f8020c0c 100644 --- a/erdpy/projects/report/data/wasm_report.py +++ b/erdpy/projects/report/data/wasm_report.py @@ -1,11 +1,11 @@ from typing import Any, List, Optional -from erdpy.projects.report.data.common import first_non_none, merge_values_by_key -from erdpy.projects.report.data.option_results import OptionResults, merge_lists_of_option_results +from erdpy.projects.report.data.common import first_not_none, merge_values_by_key +from erdpy.projects.report.data.option_results import ExtractedFeature, merge_lists_of_option_results from erdpy.projects.report.format.format_options import FormatOptions class WasmReport: - def __init__(self, wasm_name: str, option_results: List[OptionResults]) -> None: + def __init__(self, wasm_name: str, option_results: List[ExtractedFeature]) -> None: self.wasm_name = wasm_name self.option_results = option_results @@ -14,10 +14,10 @@ def to_json(self) -> Any: 'wasm_name': self.wasm_name, 'option_results': self.option_results } - + @staticmethod def from_json(json: Any) -> 'WasmReport': - option_results = [OptionResults.from_json(option_result) for option_result in json['option_results']] + option_results = [ExtractedFeature.from_json(option_result) for option_result in json['option_results']] return WasmReport(json['wasm_name'], option_results) def get_option_results(self, format_options: FormatOptions) -> List[str]: @@ -32,14 +32,14 @@ def get_wasm_key(wasm: WasmReport) -> str: return wasm.wasm_name -def get_option_results_or_default(wasm: Optional[WasmReport]) -> List[OptionResults]: +def get_option_results_or_default(wasm: Optional[WasmReport]) -> List[ExtractedFeature]: if wasm is None: return [] return wasm.option_results def merge_two_wasms(first: Optional[WasmReport], second: Optional[WasmReport]) -> WasmReport: - any = first_non_none(first, second) + any = first_not_none(first, second) first_option_results = get_option_results_or_default(first) second_option_results = get_option_results_or_default(second) merged_option_results = merge_lists_of_option_results(first_option_results, second_option_results) diff --git a/erdpy/projects/report/options/__init__.py b/erdpy/projects/report/features/__init__.py similarity index 100% rename from erdpy/projects/report/options/__init__.py rename to erdpy/projects/report/features/__init__.py diff --git a/erdpy/projects/report/features/features.py b/erdpy/projects/report/features/features.py new file mode 100644 index 00000000..d41b9a66 --- /dev/null +++ b/erdpy/projects/report/features/features.py @@ -0,0 +1,13 @@ +from typing import List + +from erdpy.projects.report.features.report_option import ReportFeature +from erdpy.projects.report.features.size import Size +from erdpy.projects.report.features.twiggy_paths_check import TwiggyPathsCheck + + +def get_default_report_features() -> List[ReportFeature]: + return [ + Size("size"), + TwiggyPathsCheck("has-allocator", pattern="wee_alloc::"), + TwiggyPathsCheck("has-format", pattern="core::fmt"), + ] diff --git a/erdpy/projects/report/options/report_option.py b/erdpy/projects/report/features/report_option.py similarity index 53% rename from erdpy/projects/report/options/report_option.py rename to erdpy/projects/report/features/report_option.py index 05b746d6..67f87cbe 100644 --- a/erdpy/projects/report/options/report_option.py +++ b/erdpy/projects/report/features/report_option.py @@ -3,12 +3,20 @@ from typing import Any, Optional -class ReportOption: +class ReportFeature: + """ + Base class for any feature in a report. + + A feature represents a column in a report. + The name argument will appear as the column header in a report. + The implementation of the extract method will determine the contents of each cell in said column. + """ + def __init__(self, name: str) -> None: self.name = name @abstractmethod - def apply(self, wasm_path: Path) -> str: + def extract(self, wasm_path: Path) -> str: pass def requires_twiggy_paths(self) -> bool: diff --git a/erdpy/projects/report/options/size.py b/erdpy/projects/report/features/size.py similarity index 70% rename from erdpy/projects/report/options/size.py rename to erdpy/projects/report/features/size.py index fea10ceb..f9503b9c 100644 --- a/erdpy/projects/report/options/size.py +++ b/erdpy/projects/report/features/size.py @@ -1,11 +1,11 @@ from pathlib import Path from typing import Optional -from .report_option import ReportOption, str_or_default +from .report_option import ReportFeature, str_or_default -class Size(ReportOption): - def apply(self, wasm_path: Path): +class Size(ReportFeature): + def extract(self, wasm_path: Path): size = get_file_size(wasm_path) return str_or_default(size) diff --git a/erdpy/projects/report/options/twiggy_paths_check.py b/erdpy/projects/report/features/twiggy_paths_check.py similarity index 61% rename from erdpy/projects/report/options/twiggy_paths_check.py rename to erdpy/projects/report/features/twiggy_paths_check.py index 3fdbf752..1cb26b2c 100644 --- a/erdpy/projects/report/options/twiggy_paths_check.py +++ b/erdpy/projects/report/features/twiggy_paths_check.py @@ -3,57 +3,56 @@ from erdpy import dependencies, myprocess, utils from erdpy.errors import BadFile -from erdpy.projects.report.options.report_option import ReportOption +from erdpy.projects.report.features.report_option import ReportFeature logger = logging.getLogger("projects.report.options.twiggy_paths_check") -class TwiggyPathsCheck(ReportOption): +class TwiggyPathsCheck(ReportFeature): def __init__(self, name: str, pattern: str) -> None: super().__init__(name) self.pattern = pattern - - def apply(self, wasm_path: Path) -> str: - twiggy_paths_path = get_twiggy_paths_path(wasm_path) + def extract(self, wasm_path: Path) -> str: + twiggy_paths_path = _get_twiggy_paths_path(wasm_path) try: text = utils.read_text_file(twiggy_paths_path) return str(self.pattern in text) except BadFile: return 'N/A' - + def requires_twiggy_paths(self): return True -def replace_file_suffix(file_path: Path, suffix: str) -> Path: +def run_twiggy_paths(wasm_path: Path) -> Path: + rust = dependencies.get_module_by_key("rust") + debug_wasm_path = _get_debug_wasm_path(wasm_path) + twiggy_paths_args = ["twiggy", "paths", str(debug_wasm_path)] + output = myprocess.run_process(twiggy_paths_args, env=rust.get_env(), cwd=debug_wasm_path.parent, dump_to_stdout=False) + output_path = _get_twiggy_paths_path(wasm_path) + utils.write_file(output_path, output) + logger.info(f"Twiggy paths output path: {output_path}") + return output_path + + +def _replace_file_suffix(file_path: Path, suffix: str) -> Path: new_name = file_path.stem + suffix return file_path.with_name(new_name) -def get_debug_wasm_path(wasm_path: Path) -> Path: +def _get_debug_wasm_path(wasm_path: Path) -> Path: """ ->>> get_debug_wasm_path(Path('test/contract.wasm')) +>>> _get_debug_wasm_path(Path('test/contract.wasm')) PosixPath('test/contract-dbg.wasm') """ - return replace_file_suffix(wasm_path, '-dbg.wasm') + return _replace_file_suffix(wasm_path, '-dbg.wasm') -def get_twiggy_paths_path(wasm_path: Path) -> Path: +def _get_twiggy_paths_path(wasm_path: Path) -> Path: """ ->>> replace_file_suffix(Path('test/contract.wasm'), '-paths.txt') +>>> _replace_file_suffix(Path('test/contract.wasm'), '-paths.txt') PosixPath('test/contract-paths.txt') """ - return replace_file_suffix(wasm_path, '-paths.txt') - - -def run_twiggy_paths(wasm_path: Path) -> Path: - rust = dependencies.get_module_by_key("rust") - debug_wasm_path = get_debug_wasm_path(wasm_path) - twiggy_paths_args = ["twiggy", "paths", str(debug_wasm_path)] - output = myprocess.run_process(twiggy_paths_args, env=rust.get_env(), cwd=debug_wasm_path.parent, dump_to_stdout=False) - output_path = get_twiggy_paths_path(wasm_path) - utils.write_file(output_path, output) - logger.info(f"Twiggy paths output path: {output_path}") - return output_path + return _replace_file_suffix(wasm_path, '-paths.txt') diff --git a/erdpy/projects/report/options/builder.py b/erdpy/projects/report/options/builder.py deleted file mode 100644 index 068b07f6..00000000 --- a/erdpy/projects/report/options/builder.py +++ /dev/null @@ -1,11 +0,0 @@ -from typing import List -from .report_option import ReportOption -from .size import Size -from .twiggy_paths_check import TwiggyPathsCheck - -def get_default_report_options() -> List[ReportOption]: - return [ - Size("size"), - TwiggyPathsCheck("has-allocator", pattern="wee_alloc::"), - TwiggyPathsCheck("has-format", pattern="core::fmt"), - ] diff --git a/erdpy/projects/report/report_cli.py b/erdpy/projects/report/report_cli.py index e4389c11..6cf58c15 100644 --- a/erdpy/projects/report/report_cli.py +++ b/erdpy/projects/report/report_cli.py @@ -6,12 +6,13 @@ from erdpy.projects.core import get_project_paths_recursively from erdpy.projects.report.data.report import Report, merge_list_of_reports from erdpy.projects.report.format.format_options import FormatOptions -from erdpy.projects.report.options.builder import get_default_report_options +from erdpy.projects.report.features.features import get_default_report_features from erdpy.projects.report.report_creator import ReportCreator logger = logging.getLogger("report") + def report_cli(args: Any) -> None: compare_report_paths = args.compare if compare_report_paths is None: @@ -23,7 +24,7 @@ def report_cli(args: Any) -> None: def build_report_cli(args: Any) -> None: base_path = Path(args.project) project_paths = get_project_paths_recursively(base_path) - options = get_default_report_options() + options = get_default_report_features() report_creator = ReportCreator(options, skip_build=args.skip_build, skip_twiggy=args.skip_twiggy) report = report_creator.create_report(base_path, project_paths) finalize_report(report, args) diff --git a/erdpy/projects/report/report_creator.py b/erdpy/projects/report/report_creator.py index 7bcc2467..69684570 100644 --- a/erdpy/projects/report/report_creator.py +++ b/erdpy/projects/report/report_creator.py @@ -5,7 +5,7 @@ from typing import Iterable, List, Tuple from erdpy import guards from erdpy.projects.report.data.folder_report import FolderReport -from erdpy.projects.report.data.option_results import OptionResults +from erdpy.projects.report.data.option_results import ExtractedFeature from erdpy.projects.report.data.report import Report from erdpy.projects.report.data.wasm_report import WasmReport from erdpy.projects.report.data.project_report import ProjectReport @@ -13,32 +13,35 @@ from erdpy.projects.project_base import remove_suffix from erdpy.projects.project_rust import ProjectRust -from erdpy.projects.report.options.report_option import ReportOption -from erdpy.projects.report.options.twiggy_paths_check import run_twiggy_paths +from erdpy.projects.report.features.report_option import ReportFeature +from erdpy.projects.report.features.twiggy_paths_check import run_twiggy_paths class ReportCreator: - def __init__(self, options: List[ReportOption], skip_build: bool, skip_twiggy: bool) -> None: + def __init__(self, options: List[ReportFeature], skip_build: bool, skip_twiggy: bool) -> None: self.options = options self.skip_build = skip_build self.skip_twiggy = skip_twiggy self.require_twiggy_paths = any(option.requires_twiggy_paths() for option in self.options) + def create_report(self, base_path: Path, project_paths: List[Path]) -> Report: + base_path = base_path.resolve() + guards.is_directory(base_path) - def apply_option(self, option: ReportOption, wasm_path: Path) -> OptionResults: - result = option.apply(wasm_path) - return OptionResults(option.name, [result]) + folder_groups = [self._create_folder_report(base_path, parent_folder, iter) + for parent_folder, iter in group_projects_by_folder(project_paths)] + option_names = [option.name for option in self.options] + return Report(option_names, folder_groups) - def create_wasm_report(self, wasm_path: Path, twiggy_requirements_met: bool) -> WasmReport: - if twiggy_requirements_met: - run_twiggy_paths(wasm_path) - name = wasm_path.name - option_results = [self.apply_option(option, wasm_path) for option in self.options] - return WasmReport(name, option_results) + def _create_folder_report(self, base_path: Path, parent_folder: Path, iter: Iterable[Tuple[Path, Path]]) -> FolderReport: + parent_folder = parent_folder.resolve() + project_reports = [self._create_project_report(parent_folder, project_path) for _, project_path in iter] + root_path = parent_folder.relative_to(base_path.parent) + return FolderReport(root_path, project_reports) - def create_project_report(self, parent_path: Path, project_path: Path) -> ProjectReport: + def _create_project_report(self, parent_path: Path, project_path: Path) -> ProjectReport: project_path = project_path.resolve() project = load_project(project_path) @@ -51,29 +54,22 @@ def create_project_report(self, parent_path: Path, project_path: Path) -> Projec project.build_wasm_with_debug_symbols() twiggy_requirements_met = True - wasm_reports = [self.create_wasm_report(wasm_path, twiggy_requirements_met) for wasm_path in project.find_wasm_files()] + wasm_reports = [self._create_wasm_report(wasm_path, twiggy_requirements_met) for wasm_path in project.find_wasm_files()] wasm_reports.sort(key=lambda report: remove_suffix(report.wasm_name, '.wasm')) return ProjectReport(project_path.relative_to(parent_path), wasm_reports) + def _create_wasm_report(self, wasm_path: Path, twiggy_requirements_met: bool) -> WasmReport: + if twiggy_requirements_met: + run_twiggy_paths(wasm_path) + name = wasm_path.name + option_results = [_extract_feature(option, wasm_path) for option in self.options] + return WasmReport(name, option_results) - def create_folder_report(self, base_path: Path, parent_folder: Path, iter: Iterable[Tuple[Path, Path]]) -> FolderReport: - parent_folder = parent_folder.resolve() - project_reports = [self.create_project_report(parent_folder, project_path) for _, project_path in iter] - - root_path = parent_folder.relative_to(base_path.parent) - return FolderReport(root_path, project_reports) - - - def create_report(self, base_path: Path, project_paths: List[Path]) -> Report: - base_path = base_path.resolve() - guards.is_directory(base_path) - - folder_groups = [self.create_folder_report(base_path, parent_folder, iter) - for parent_folder, iter in group_projects_by_folder(project_paths)] - option_names = [option.name for option in self.options] - return Report(option_names, folder_groups) +def _extract_feature(feature: ReportFeature, wasm_path: Path) -> ExtractedFeature: + result = feature.extract(wasm_path) + return ExtractedFeature(feature.name, [result]) def group_projects_by_folder(project_paths: List[Path]) -> Iterable[Tuple[Path, Iterable[Tuple[Path, Path]]]]: From 0c0c29938eef9c2c3a3673e6808c9b5537c7db03 Mon Sep 17 00:00:00 2001 From: Claudiu-Marcel Bruda Date: Mon, 28 Feb 2022 10:43:06 +0200 Subject: [PATCH 36/44] rename wasms to wasm_reports --- erdpy/projects/report/data/project_report.py | 32 ++++++++++---------- erdpy/projects/report/data/wasm_report.py | 6 ++-- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/erdpy/projects/report/data/project_report.py b/erdpy/projects/report/data/project_report.py index ed291184..131feacc 100644 --- a/erdpy/projects/report/data/project_report.py +++ b/erdpy/projects/report/data/project_report.py @@ -2,35 +2,35 @@ from typing import Any, List, Optional from erdpy.projects.report.data.common import first_not_none, merge_values_by_key -from erdpy.projects.report.data.wasm_report import WasmReport, merge_list_of_wasms +from erdpy.projects.report.data.wasm_report import WasmReport, merge_list_of_wasm_reports from erdpy.projects.report.format.format_options import FormatOptions class ProjectReport: - def __init__(self, project_path: Path, wasms: List[WasmReport]) -> None: + def __init__(self, project_path: Path, wasm_reports: List[WasmReport]) -> None: self.project_path = project_path - self.wasms = wasms + self.wasm_reports = wasm_reports def to_json(self) -> Any: return { - "project_path": str(self.project_path), - 'wasms': self.wasms + 'project_path': str(self.project_path), + 'wasm_reports': self.wasm_reports } @staticmethod def from_json(json: Any) -> 'ProjectReport': - wasms = [WasmReport.from_json(wasm) for wasm in json['wasms']] - return ProjectReport(Path(json['project_path']), wasms) + wasm_reports = [WasmReport.from_json(wasm_report) for wasm_report in json['wasm_reports']] + return ProjectReport(Path(json['project_path']), wasm_reports) def get_rows_markdown(self, format_options: FormatOptions) -> List[List[str]]: - wasm_count = len(self.wasms) + wasm_count = len(self.wasm_reports) if wasm_count == 0: return [[f" - {str(self.project_path)} "]] elif wasm_count == 1: - return [[f" - {str(self.project_path / self.wasms[0].wasm_name)}"] + self.wasms[0].get_option_results(format_options)] + return [[f" - {str(self.project_path / self.wasm_reports[0].wasm_name)}"] + self.wasm_reports[0].get_option_results(format_options)] else: project_path_row = [f" - {str(self.project_path)}"] - wasm_rows = [[f" - - {wasm.wasm_name}"] + wasm.get_option_results(format_options) for wasm in self.wasms] + wasm_rows = [[f" - - {wasm.wasm_name}"] + wasm.get_option_results(format_options) for wasm in self.wasm_reports] return [project_path_row] + wasm_rows @@ -42,15 +42,15 @@ def get_project_report_path(project_report: ProjectReport) -> Path: return project_report.project_path -def wasms_or_default(project_report: Optional[ProjectReport]) -> List[WasmReport]: +def wasm_reports_or_default(project_report: Optional[ProjectReport]) -> List[WasmReport]: if project_report is None: return [] - return project_report.wasms + return project_report.wasm_reports def merge_two_project_reports(first: Optional[ProjectReport], second: Optional[ProjectReport]) -> ProjectReport: any = first_not_none(first, second) - first_wasms = wasms_or_default(first) - second_wasms = wasms_or_default(second) - merged_wasms = merge_list_of_wasms(first_wasms, second_wasms) - return ProjectReport(any.project_path, merged_wasms) + first_wasm_reports = wasm_reports_or_default(first) + second_wasm_reports = wasm_reports_or_default(second) + merged_wasm_reports = merge_list_of_wasm_reports(first_wasm_reports, second_wasm_reports) + return ProjectReport(any.project_path, merged_wasm_reports) diff --git a/erdpy/projects/report/data/wasm_report.py b/erdpy/projects/report/data/wasm_report.py index f8020c0c..57b9a07d 100644 --- a/erdpy/projects/report/data/wasm_report.py +++ b/erdpy/projects/report/data/wasm_report.py @@ -24,8 +24,8 @@ def get_option_results(self, format_options: FormatOptions) -> List[str]: return [option.results_to_markdown(format_options) for option in self.option_results] -def merge_list_of_wasms(first: List[WasmReport], second: List[WasmReport]) -> List[WasmReport]: - return merge_values_by_key(first, second, get_wasm_key, merge_two_wasms) +def merge_list_of_wasm_reports(first: List[WasmReport], second: List[WasmReport]) -> List[WasmReport]: + return merge_values_by_key(first, second, get_wasm_key, merge_two_wasm_reports) def get_wasm_key(wasm: WasmReport) -> str: @@ -38,7 +38,7 @@ def get_option_results_or_default(wasm: Optional[WasmReport]) -> List[ExtractedF return wasm.option_results -def merge_two_wasms(first: Optional[WasmReport], second: Optional[WasmReport]) -> WasmReport: +def merge_two_wasm_reports(first: Optional[WasmReport], second: Optional[WasmReport]) -> WasmReport: any = first_not_none(first, second) first_option_results = get_option_results_or_default(first) second_option_results = get_option_results_or_default(second) From d3e069eed236175162f48bb3bee78193cd6600ff Mon Sep 17 00:00:00 2001 From: Claudiu-Marcel Bruda Date: Mon, 28 Feb 2022 10:47:36 +0200 Subject: [PATCH 37/44] remove _cli suffix --- erdpy/cli_contracts.py | 2 +- erdpy/projects/__init__.py | 4 ++-- erdpy/projects/report/__init__.py | 4 ++-- erdpy/projects/report/{report_cli.py => do_report.py} | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) rename erdpy/projects/report/{report_cli.py => do_report.py} (95%) diff --git a/erdpy/cli_contracts.py b/erdpy/cli_contracts.py index 6ffcb531..1a5cae56 100644 --- a/erdpy/cli_contracts.py +++ b/erdpy/cli_contracts.py @@ -67,7 +67,7 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: sub.add_argument("--output-format", type=str, default="text-markdown", choices=["github-markdown", "text-markdown", "json"], help="report output format (default: %(default)s)") sub.add_argument("--output-file", type=Path, help="if specified, the output is written to a file, otherwise it's written to the standard output") sub.add_argument("--compare", type=Path, nargs='+', metavar=("report-1.json", "report-2.json"), help="create a comparison from two or more reports") - sub.set_defaults(func=projects.report_cli) + sub.set_defaults(func=projects.do_report) output_description = CLIOutputBuilder.describe(with_contract=True, with_transaction_on_network=True, with_simulation=True) sub = cli_shared.add_command_subparser(subparsers, "contract", "deploy", f"Deploy a Smart Contract.{output_description}") diff --git a/erdpy/projects/__init__.py b/erdpy/projects/__init__.py index 2bdf7dad..5b7b0033 100644 --- a/erdpy/projects/__init__.py +++ b/erdpy/projects/__init__.py @@ -6,8 +6,8 @@ from erdpy.projects.project_cpp import ProjectCpp from erdpy.projects.project_rust import ProjectRust from erdpy.projects.project_sol import ProjectSol -from erdpy.projects.report.report_cli import report_cli +from erdpy.projects.report.do_report import do_report from erdpy.projects.templates import (create_from_template, list_project_templates) -__all__ = ["build_project", "clean_project", "report_cli", "run_tests", "get_projects_in_workspace", "load_project", "Project", "ProjectClang", "ProjectCpp", "ProjectRust", "ProjectSol", "create_from_template", "list_project_templates"] +__all__ = ["build_project", "clean_project", "do_report", "run_tests", "get_projects_in_workspace", "load_project", "Project", "ProjectClang", "ProjectCpp", "ProjectRust", "ProjectSol", "create_from_template", "list_project_templates"] diff --git a/erdpy/projects/report/__init__.py b/erdpy/projects/report/__init__.py index 35cd7aa1..54ebbb27 100644 --- a/erdpy/projects/report/__init__.py +++ b/erdpy/projects/report/__init__.py @@ -1,3 +1,3 @@ -from .report_cli import report_cli +from .do_report import do_report -__all__ = ["report_cli"] +__all__ = ["do_report"] diff --git a/erdpy/projects/report/report_cli.py b/erdpy/projects/report/do_report.py similarity index 95% rename from erdpy/projects/report/report_cli.py rename to erdpy/projects/report/do_report.py index 6cf58c15..50b12e5d 100644 --- a/erdpy/projects/report/report_cli.py +++ b/erdpy/projects/report/do_report.py @@ -13,15 +13,15 @@ logger = logging.getLogger("report") -def report_cli(args: Any) -> None: +def do_report(args: Any) -> None: compare_report_paths = args.compare if compare_report_paths is None: - build_report_cli(args) + build_report(args) else: compare_reports_cli(args, compare_report_paths) -def build_report_cli(args: Any) -> None: +def build_report(args: Any) -> None: base_path = Path(args.project) project_paths = get_project_paths_recursively(base_path) options = get_default_report_features() From bb95ca94ffaedf225857af371fa95ff813fa0e1a Mon Sep 17 00:00:00 2001 From: Claudiu-Marcel Bruda Date: Mon, 28 Feb 2022 10:57:08 +0200 Subject: [PATCH 38/44] private functions --- erdpy/projects/report/data/folder_report.py | 12 +++---- erdpy/projects/report/data/project_report.py | 12 +++---- erdpy/projects/report/data/report.py | 34 ++++++++++---------- erdpy/projects/report/data/wasm_report.py | 12 +++---- erdpy/projects/report/do_report.py | 22 ++++++------- erdpy/projects/report/features/size.py | 4 +-- erdpy/projects/report/format/change_type.py | 10 +++--- erdpy/projects/report/report_creator.py | 4 +-- 8 files changed, 55 insertions(+), 55 deletions(-) diff --git a/erdpy/projects/report/data/folder_report.py b/erdpy/projects/report/data/folder_report.py index bc54de2c..26abc431 100644 --- a/erdpy/projects/report/data/folder_report.py +++ b/erdpy/projects/report/data/folder_report.py @@ -28,22 +28,22 @@ def get_markdown_rows(self, format_options: FormatOptions) -> List[List[str]]: def merge_list_of_folder_reports(first: List[FolderReport], second: List[FolderReport]) -> List[FolderReport]: - return merge_values_by_key(first, second, get_folder_report_root_path, merge_two_folder_reports) + return merge_values_by_key(first, second, _get_folder_report_root_path, _merge_two_folder_reports) -def get_folder_report_root_path(item: FolderReport) -> Path: +def _get_folder_report_root_path(item: FolderReport) -> Path: return item.root_path -def projects_or_default(folder_report: Optional[FolderReport]) -> List[ProjectReport]: +def _projects_or_default(folder_report: Optional[FolderReport]) -> List[ProjectReport]: if folder_report is None: return [] return folder_report.projects -def merge_two_folder_reports(first: Optional[FolderReport], second: Optional[FolderReport]) -> FolderReport: +def _merge_two_folder_reports(first: Optional[FolderReport], second: Optional[FolderReport]) -> FolderReport: any = first_not_none(first, second) - first_projects = projects_or_default(first) - second_projects = projects_or_default(second) + first_projects = _projects_or_default(first) + second_projects = _projects_or_default(second) merged_projects = merge_list_of_projects(first_projects, second_projects) return FolderReport(any.root_path, merged_projects) diff --git a/erdpy/projects/report/data/project_report.py b/erdpy/projects/report/data/project_report.py index 131feacc..a638f21f 100644 --- a/erdpy/projects/report/data/project_report.py +++ b/erdpy/projects/report/data/project_report.py @@ -35,22 +35,22 @@ def get_rows_markdown(self, format_options: FormatOptions) -> List[List[str]]: def merge_list_of_projects(first: List[ProjectReport], second: List[ProjectReport]) -> List[ProjectReport]: - return merge_values_by_key(first, second, get_project_report_path, merge_two_project_reports) + return merge_values_by_key(first, second, _get_project_report_path, _merge_two_project_reports) -def get_project_report_path(project_report: ProjectReport) -> Path: +def _get_project_report_path(project_report: ProjectReport) -> Path: return project_report.project_path -def wasm_reports_or_default(project_report: Optional[ProjectReport]) -> List[WasmReport]: +def _wasm_reports_or_default(project_report: Optional[ProjectReport]) -> List[WasmReport]: if project_report is None: return [] return project_report.wasm_reports -def merge_two_project_reports(first: Optional[ProjectReport], second: Optional[ProjectReport]) -> ProjectReport: +def _merge_two_project_reports(first: Optional[ProjectReport], second: Optional[ProjectReport]) -> ProjectReport: any = first_not_none(first, second) - first_wasm_reports = wasm_reports_or_default(first) - second_wasm_reports = wasm_reports_or_default(second) + first_wasm_reports = _wasm_reports_or_default(first) + second_wasm_reports = _wasm_reports_or_default(second) merged_wasm_reports = merge_list_of_wasm_reports(first_wasm_reports, second_wasm_reports) return ProjectReport(any.project_path, merged_wasm_reports) diff --git a/erdpy/projects/report/data/report.py b/erdpy/projects/report/data/report.py index 1d5c784a..9f0874ce 100644 --- a/erdpy/projects/report/data/report.py +++ b/erdpy/projects/report/data/report.py @@ -40,16 +40,16 @@ def to_markdown(self, format_options: FormatOptions) -> str: text = StringIO() table_headers = ["Path"] + self.option_names - adjust_table_headers(table_headers, format_options) - write_markdown_row(text, table_headers, format_options) + _adjust_table_headers(table_headers, format_options) + _write_markdown_row(text, table_headers, format_options) ALIGN_LEFT = ":--" ALIGN_RIGHT = "--:" row_alignments = [ALIGN_LEFT] + len(self.option_names) * [ALIGN_RIGHT] - write_markdown_row(text, row_alignments, format_options) + _write_markdown_row(text, row_alignments, format_options) for row in self.get_markdown_rows(format_options): - write_markdown_row(text, row, format_options) + _write_markdown_row(text, row, format_options) return text.getvalue() @@ -59,7 +59,7 @@ def to_json_string(self) -> str: # Adjusts the column widths in github tables - see: # https://github.com/markedjs/marked/issues/266#issuecomment-616347986 -def adjust_table_headers(table_headers: List[str], format_options: FormatOptions) -> None: +def _adjust_table_headers(table_headers: List[str], format_options: FormatOptions) -> None: if not format_options.github_flavor: return NBSP = '\u00A0' @@ -70,10 +70,10 @@ def adjust_table_headers(table_headers: List[str], format_options: FormatOptions def merge_list_of_reports(reports: List[Report]) -> Report: - return functools.reduce(merge_two_reports, reports) + return functools.reduce(_merge_two_reports, reports) -def merge_two_reports(first: Report, other: Report) -> Report: +def _merge_two_reports(first: Report, other: Report) -> Report: option_names = merge_values(first.option_names, other.option_names) folders = merge_list_of_folder_reports(first.folders, other.folders) return Report(option_names, folders) @@ -81,27 +81,27 @@ def merge_two_reports(first: Report, other: Report) -> Report: # Hack in order to keep the column alignment in a terminal # as unicode characters are sometimes wider or narrower -def justify_text_string(string: str, width: int) -> str: - if ChangeType.UNKNOWN.to_text_markdown() in string: +def _justify_text_string(string: str, width: int) -> str: + if ChangeType.UNKNOWN._to_text_markdown() in string: width += 1 - if ChangeType.GOOD.to_text_markdown() in string: + if ChangeType.GOOD._to_text_markdown() in string: width -= 1 - if ChangeType.BAD.to_text_markdown() in string: + if ChangeType.BAD._to_text_markdown() in string: width -= 1 return string.rjust(width) -def format_row_markdown(row: List[str], format_options: FormatOptions) -> str: +def _format_row_markdown(row: List[str], format_options: FormatOptions) -> str: row += [''] * (4 - len(row)) if not format_options.github_flavor: row[0] = row[0].ljust(100) - row[1] = justify_text_string(row[1], 20) - row[2] = justify_text_string(row[2], 20) - row[3] = justify_text_string(row[3], 20) + row[1] = _justify_text_string(row[1], 20) + row[2] = _justify_text_string(row[2], 20) + row[3] = _justify_text_string(row[3], 20) merged_cells = " | ".join(row) return f"| {merged_cells} |" -def write_markdown_row(string: StringIO, row: List[str], format_options: FormatOptions): - string.write(format_row_markdown(row, format_options)) +def _write_markdown_row(string: StringIO, row: List[str], format_options: FormatOptions): + string.write(_format_row_markdown(row, format_options)) string.write('\n') diff --git a/erdpy/projects/report/data/wasm_report.py b/erdpy/projects/report/data/wasm_report.py index 57b9a07d..ca4bcd58 100644 --- a/erdpy/projects/report/data/wasm_report.py +++ b/erdpy/projects/report/data/wasm_report.py @@ -25,14 +25,14 @@ def get_option_results(self, format_options: FormatOptions) -> List[str]: def merge_list_of_wasm_reports(first: List[WasmReport], second: List[WasmReport]) -> List[WasmReport]: - return merge_values_by_key(first, second, get_wasm_key, merge_two_wasm_reports) + return merge_values_by_key(first, second, _get_wasm_report_key, merge_two_wasm_reports) -def get_wasm_key(wasm: WasmReport) -> str: - return wasm.wasm_name +def _get_wasm_report_key(wasm_report: WasmReport) -> str: + return wasm_report.wasm_name -def get_option_results_or_default(wasm: Optional[WasmReport]) -> List[ExtractedFeature]: +def _get_option_results_or_default(wasm: Optional[WasmReport]) -> List[ExtractedFeature]: if wasm is None: return [] return wasm.option_results @@ -40,7 +40,7 @@ def get_option_results_or_default(wasm: Optional[WasmReport]) -> List[ExtractedF def merge_two_wasm_reports(first: Optional[WasmReport], second: Optional[WasmReport]) -> WasmReport: any = first_not_none(first, second) - first_option_results = get_option_results_or_default(first) - second_option_results = get_option_results_or_default(second) + first_option_results = _get_option_results_or_default(first) + second_option_results = _get_option_results_or_default(second) merged_option_results = merge_lists_of_option_results(first_option_results, second_option_results) return WasmReport(any.wasm_name, merged_option_results) diff --git a/erdpy/projects/report/do_report.py b/erdpy/projects/report/do_report.py index 50b12e5d..afdfcc1d 100644 --- a/erdpy/projects/report/do_report.py +++ b/erdpy/projects/report/do_report.py @@ -16,32 +16,32 @@ def do_report(args: Any) -> None: compare_report_paths = args.compare if compare_report_paths is None: - build_report(args) + _build_report(args) else: - compare_reports_cli(args, compare_report_paths) + _compare_reports(args, compare_report_paths) -def build_report(args: Any) -> None: +def _build_report(args: Any) -> None: base_path = Path(args.project) project_paths = get_project_paths_recursively(base_path) options = get_default_report_features() report_creator = ReportCreator(options, skip_build=args.skip_build, skip_twiggy=args.skip_twiggy) report = report_creator.create_report(base_path, project_paths) - finalize_report(report, args) + _finalize_report(report, args) -def compare_reports_cli(args: Any, merge_report_paths: List[Path]) -> None: +def _compare_reports(args: Any, merge_report_paths: List[Path]) -> None: reports = [Report.load_from_file(report_path) for report_path in merge_report_paths] final_report = merge_list_of_reports(reports) - finalize_report(final_report, args) + _finalize_report(final_report, args) -def finalize_report(report: Report, args: Any) -> None: - output = get_report_output_string(report, args) - store_output(output, args) +def _finalize_report(report: Report, args: Any) -> None: + output = _get_report_output_string(report, args) + _store_output(output, args) -def get_report_output_string(report: Report, args: Any) -> str: +def _get_report_output_string(report: Report, args: Any) -> str: output_format = args.output_format if output_format == 'github-markdown': return report.to_markdown(FormatOptions(github_markdown=True)) @@ -52,7 +52,7 @@ def get_report_output_string(report: Report, args: Any) -> str: raise Exception('Invalid output format') -def store_output(output: str, args: Any) -> None: +def _store_output(output: str, args: Any) -> None: output_file_path = args.output_file if output_file_path is None: print(output) diff --git a/erdpy/projects/report/features/size.py b/erdpy/projects/report/features/size.py index f9503b9c..2b162d22 100644 --- a/erdpy/projects/report/features/size.py +++ b/erdpy/projects/report/features/size.py @@ -6,11 +6,11 @@ class Size(ReportFeature): def extract(self, wasm_path: Path): - size = get_file_size(wasm_path) + size = _get_file_size(wasm_path) return str_or_default(size) -def get_file_size(file_path: Path) -> Optional[int]: +def _get_file_size(file_path: Path) -> Optional[int]: try: return int(file_path.stat().st_size) except FileNotFoundError: diff --git a/erdpy/projects/report/format/change_type.py b/erdpy/projects/report/format/change_type.py index 59f098c1..0929e432 100644 --- a/erdpy/projects/report/format/change_type.py +++ b/erdpy/projects/report/format/change_type.py @@ -12,11 +12,11 @@ class ChangeType(Enum): def to_markdown(self, format_options: FormatOptions) -> str: if format_options.github_flavor: - return self.to_github_markdown() + return self._to_github_markdown() else: - return self.to_text_markdown() + return self._to_text_markdown() - def to_github_markdown(self) -> str: + def _to_github_markdown(self) -> str: switch = { ChangeType.UNKNOWN: ':warning:', ChangeType.NONE: '', @@ -25,8 +25,8 @@ def to_github_markdown(self) -> str: ChangeType.MIXED: ':yellow_circle:' } return switch[self] - - def to_text_markdown(self) -> str: + + def _to_text_markdown(self) -> str: switch = { ChangeType.UNKNOWN: '\u26a0\ufe0f ', ChangeType.NONE: '', diff --git a/erdpy/projects/report/report_creator.py b/erdpy/projects/report/report_creator.py index 69684570..519a663e 100644 --- a/erdpy/projects/report/report_creator.py +++ b/erdpy/projects/report/report_creator.py @@ -29,7 +29,7 @@ def create_report(self, base_path: Path, project_paths: List[Path]) -> Report: guards.is_directory(base_path) folder_groups = [self._create_folder_report(base_path, parent_folder, iter) - for parent_folder, iter in group_projects_by_folder(project_paths)] + for parent_folder, iter in _group_projects_by_folder(project_paths)] option_names = [option.name for option in self.options] return Report(option_names, folder_groups) @@ -72,6 +72,6 @@ def _extract_feature(feature: ReportFeature, wasm_path: Path) -> ExtractedFeatur return ExtractedFeature(feature.name, [result]) -def group_projects_by_folder(project_paths: List[Path]) -> Iterable[Tuple[Path, Iterable[Tuple[Path, Path]]]]: +def _group_projects_by_folder(project_paths: List[Path]) -> Iterable[Tuple[Path, Iterable[Tuple[Path, Path]]]]: path_pairs = sorted([(path.parent, path) for path in project_paths]) return itertools.groupby(path_pairs, operator.itemgetter(0)) From e7c88554514a455be18d240ce7df1cfc43fd7cfa Mon Sep 17 00:00:00 2001 From: Claudiu-Marcel Bruda Date: Mon, 28 Feb 2022 11:08:55 +0200 Subject: [PATCH 39/44] rename options to features --- erdpy/projects/report/data/option_results.py | 10 +++---- erdpy/projects/report/data/project_report.py | 4 +-- erdpy/projects/report/data/report.py | 16 +++++------ erdpy/projects/report/data/wasm_report.py | 28 ++++++++++---------- erdpy/projects/report/report_creator.py | 12 ++++----- 5 files changed, 35 insertions(+), 35 deletions(-) diff --git a/erdpy/projects/report/data/option_results.py b/erdpy/projects/report/data/option_results.py index bf035344..23110884 100644 --- a/erdpy/projects/report/data/option_results.py +++ b/erdpy/projects/report/data/option_results.py @@ -83,15 +83,15 @@ def _is_strictly_worse(items: List[Any]) -> bool: return sorted(items, reverse=True) == items -def merge_lists_of_option_results(first: List[ExtractedFeature], second: List[ExtractedFeature]) -> List[ExtractedFeature]: - return merge_values_by_key(first, second, _get_option_result_key, _merge_two_option_results) +def merge_lists_of_extracted_features(first: List[ExtractedFeature], second: List[ExtractedFeature]) -> List[ExtractedFeature]: + return merge_values_by_key(first, second, _get_extracted_feature_key, _merge_two_extracted_features) -def _get_option_result_key(option_results: ExtractedFeature) -> str: - return option_results.feature_name +def _get_extracted_feature_key(extracted_feature: ExtractedFeature) -> str: + return extracted_feature.feature_name -def _merge_two_option_results(first: Optional[ExtractedFeature], second: Optional[ExtractedFeature]) -> ExtractedFeature: +def _merge_two_extracted_features(first: Optional[ExtractedFeature], second: Optional[ExtractedFeature]) -> ExtractedFeature: any = first_not_none(first, second) merged_results = _results_or_NA(first) + _results_or_NA(second) return ExtractedFeature(any.feature_name, merged_results) diff --git a/erdpy/projects/report/data/project_report.py b/erdpy/projects/report/data/project_report.py index a638f21f..a66ee560 100644 --- a/erdpy/projects/report/data/project_report.py +++ b/erdpy/projects/report/data/project_report.py @@ -27,10 +27,10 @@ def get_rows_markdown(self, format_options: FormatOptions) -> List[List[str]]: if wasm_count == 0: return [[f" - {str(self.project_path)} "]] elif wasm_count == 1: - return [[f" - {str(self.project_path / self.wasm_reports[0].wasm_name)}"] + self.wasm_reports[0].get_option_results(format_options)] + return [[f" - {str(self.project_path / self.wasm_reports[0].wasm_name)}"] + self.wasm_reports[0].get_extracted_features_markdown(format_options)] else: project_path_row = [f" - {str(self.project_path)}"] - wasm_rows = [[f" - - {wasm.wasm_name}"] + wasm.get_option_results(format_options) for wasm in self.wasm_reports] + wasm_rows = [[f" - - {wasm.wasm_name}"] + wasm.get_extracted_features_markdown(format_options) for wasm in self.wasm_reports] return [project_path_row] + wasm_rows diff --git a/erdpy/projects/report/data/report.py b/erdpy/projects/report/data/report.py index 9f0874ce..c4be0437 100644 --- a/erdpy/projects/report/data/report.py +++ b/erdpy/projects/report/data/report.py @@ -11,20 +11,20 @@ class Report: - def __init__(self, option_names: List[str], folders: List[FolderReport]) -> None: - self.option_names = option_names + def __init__(self, feature_names: List[str], folders: List[FolderReport]) -> None: + self.feature_names = feature_names self.folders = folders def to_json(self) -> Any: return { - 'options': self.option_names, + 'features': self.feature_names, 'folders': self.folders } @staticmethod def from_json(json: Any) -> 'Report': folders = [FolderReport.from_json(folder_report) for folder_report in json['folders']] - return Report(json['options'], folders) + return Report(json['features'], folders) @staticmethod def load_from_file(report_json_path: Path) -> 'Report': @@ -39,13 +39,13 @@ def get_markdown_rows(self, format_options: FormatOptions) -> List[List[str]]: def to_markdown(self, format_options: FormatOptions) -> str: text = StringIO() - table_headers = ["Path"] + self.option_names + table_headers = ["Path"] + self.feature_names _adjust_table_headers(table_headers, format_options) _write_markdown_row(text, table_headers, format_options) ALIGN_LEFT = ":--" ALIGN_RIGHT = "--:" - row_alignments = [ALIGN_LEFT] + len(self.option_names) * [ALIGN_RIGHT] + row_alignments = [ALIGN_LEFT] + len(self.feature_names) * [ALIGN_RIGHT] _write_markdown_row(text, row_alignments, format_options) for row in self.get_markdown_rows(format_options): @@ -74,9 +74,9 @@ def merge_list_of_reports(reports: List[Report]) -> Report: def _merge_two_reports(first: Report, other: Report) -> Report: - option_names = merge_values(first.option_names, other.option_names) + feature_names = merge_values(first.feature_names, other.feature_names) folders = merge_list_of_folder_reports(first.folders, other.folders) - return Report(option_names, folders) + return Report(feature_names, folders) # Hack in order to keep the column alignment in a terminal diff --git a/erdpy/projects/report/data/wasm_report.py b/erdpy/projects/report/data/wasm_report.py index ca4bcd58..78153652 100644 --- a/erdpy/projects/report/data/wasm_report.py +++ b/erdpy/projects/report/data/wasm_report.py @@ -1,27 +1,27 @@ from typing import Any, List, Optional from erdpy.projects.report.data.common import first_not_none, merge_values_by_key -from erdpy.projects.report.data.option_results import ExtractedFeature, merge_lists_of_option_results +from erdpy.projects.report.data.option_results import ExtractedFeature, merge_lists_of_extracted_features from erdpy.projects.report.format.format_options import FormatOptions class WasmReport: - def __init__(self, wasm_name: str, option_results: List[ExtractedFeature]) -> None: + def __init__(self, wasm_name: str, extracted_features: List[ExtractedFeature]) -> None: self.wasm_name = wasm_name - self.option_results = option_results + self.extracted_features = extracted_features def to_json(self) -> Any: return { 'wasm_name': self.wasm_name, - 'option_results': self.option_results + 'extracted_features': self.extracted_features } @staticmethod def from_json(json: Any) -> 'WasmReport': - option_results = [ExtractedFeature.from_json(option_result) for option_result in json['option_results']] - return WasmReport(json['wasm_name'], option_results) + extracted_features = [ExtractedFeature.from_json(extracted_feature) for extracted_feature in json['extracted_features']] + return WasmReport(json['wasm_name'], extracted_features) - def get_option_results(self, format_options: FormatOptions) -> List[str]: - return [option.results_to_markdown(format_options) for option in self.option_results] + def get_extracted_features_markdown(self, format_options: FormatOptions) -> List[str]: + return [extracted_feature.results_to_markdown(format_options) for extracted_feature in self.extracted_features] def merge_list_of_wasm_reports(first: List[WasmReport], second: List[WasmReport]) -> List[WasmReport]: @@ -32,15 +32,15 @@ def _get_wasm_report_key(wasm_report: WasmReport) -> str: return wasm_report.wasm_name -def _get_option_results_or_default(wasm: Optional[WasmReport]) -> List[ExtractedFeature]: +def _get_extracted_features_or_default(wasm: Optional[WasmReport]) -> List[ExtractedFeature]: if wasm is None: return [] - return wasm.option_results + return wasm.extracted_features def merge_two_wasm_reports(first: Optional[WasmReport], second: Optional[WasmReport]) -> WasmReport: any = first_not_none(first, second) - first_option_results = _get_option_results_or_default(first) - second_option_results = _get_option_results_or_default(second) - merged_option_results = merge_lists_of_option_results(first_option_results, second_option_results) - return WasmReport(any.wasm_name, merged_option_results) + first_extracted_features = _get_extracted_features_or_default(first) + second_extracted_features = _get_extracted_features_or_default(second) + merged_extracted_features = merge_lists_of_extracted_features(first_extracted_features, second_extracted_features) + return WasmReport(any.wasm_name, merged_extracted_features) diff --git a/erdpy/projects/report/report_creator.py b/erdpy/projects/report/report_creator.py index 519a663e..593293ef 100644 --- a/erdpy/projects/report/report_creator.py +++ b/erdpy/projects/report/report_creator.py @@ -19,10 +19,10 @@ class ReportCreator: def __init__(self, options: List[ReportFeature], skip_build: bool, skip_twiggy: bool) -> None: - self.options = options + self.report_features = options self.skip_build = skip_build self.skip_twiggy = skip_twiggy - self.require_twiggy_paths = any(option.requires_twiggy_paths() for option in self.options) + self.require_twiggy_paths = any(report_feature.requires_twiggy_paths() for report_feature in self.report_features) def create_report(self, base_path: Path, project_paths: List[Path]) -> Report: base_path = base_path.resolve() @@ -31,8 +31,8 @@ def create_report(self, base_path: Path, project_paths: List[Path]) -> Report: folder_groups = [self._create_folder_report(base_path, parent_folder, iter) for parent_folder, iter in _group_projects_by_folder(project_paths)] - option_names = [option.name for option in self.options] - return Report(option_names, folder_groups) + feature_names = [report_feature.name for report_feature in self.report_features] + return Report(feature_names, folder_groups) def _create_folder_report(self, base_path: Path, parent_folder: Path, iter: Iterable[Tuple[Path, Path]]) -> FolderReport: parent_folder = parent_folder.resolve() @@ -63,8 +63,8 @@ def _create_wasm_report(self, wasm_path: Path, twiggy_requirements_met: bool) -> if twiggy_requirements_met: run_twiggy_paths(wasm_path) name = wasm_path.name - option_results = [_extract_feature(option, wasm_path) for option in self.options] - return WasmReport(name, option_results) + extracted_features = [_extract_feature(report_feature, wasm_path) for report_feature in self.report_features] + return WasmReport(name, extracted_features) def _extract_feature(feature: ReportFeature, wasm_path: Path) -> ExtractedFeature: From d81de118f534146c9a09bfa561c8a95f61155d2d Mon Sep 17 00:00:00 2001 From: Claudiu-Marcel Bruda Date: Mon, 28 Feb 2022 11:11:03 +0200 Subject: [PATCH 40/44] rename option_results.py to extracted_feature.py --- .../report/data/{option_results.py => extracted_feature.py} | 0 erdpy/projects/report/data/wasm_report.py | 2 +- erdpy/projects/report/report_creator.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename erdpy/projects/report/data/{option_results.py => extracted_feature.py} (100%) diff --git a/erdpy/projects/report/data/option_results.py b/erdpy/projects/report/data/extracted_feature.py similarity index 100% rename from erdpy/projects/report/data/option_results.py rename to erdpy/projects/report/data/extracted_feature.py diff --git a/erdpy/projects/report/data/wasm_report.py b/erdpy/projects/report/data/wasm_report.py index 78153652..3a9e2a9d 100644 --- a/erdpy/projects/report/data/wasm_report.py +++ b/erdpy/projects/report/data/wasm_report.py @@ -1,6 +1,6 @@ from typing import Any, List, Optional from erdpy.projects.report.data.common import first_not_none, merge_values_by_key -from erdpy.projects.report.data.option_results import ExtractedFeature, merge_lists_of_extracted_features +from erdpy.projects.report.data.extracted_feature import ExtractedFeature, merge_lists_of_extracted_features from erdpy.projects.report.format.format_options import FormatOptions diff --git a/erdpy/projects/report/report_creator.py b/erdpy/projects/report/report_creator.py index 593293ef..ecbf951a 100644 --- a/erdpy/projects/report/report_creator.py +++ b/erdpy/projects/report/report_creator.py @@ -5,7 +5,7 @@ from typing import Iterable, List, Tuple from erdpy import guards from erdpy.projects.report.data.folder_report import FolderReport -from erdpy.projects.report.data.option_results import ExtractedFeature +from erdpy.projects.report.data.extracted_feature import ExtractedFeature from erdpy.projects.report.data.report import Report from erdpy.projects.report.data.wasm_report import WasmReport from erdpy.projects.report.data.project_report import ProjectReport From 746ce3149f015615187c50477beafae1acaf88e4 Mon Sep 17 00:00:00 2001 From: Claudiu-Marcel Bruda Date: Mon, 28 Feb 2022 11:26:10 +0200 Subject: [PATCH 41/44] add example for merge_values_by_key --- erdpy/projects/report/data/common.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/erdpy/projects/report/data/common.py b/erdpy/projects/report/data/common.py index afd5fe67..e2983c6e 100644 --- a/erdpy/projects/report/data/common.py +++ b/erdpy/projects/report/data/common.py @@ -28,6 +28,22 @@ def list_as_key_value_dict(items: List[T], key_getter: Callable[[T], K]) -> 'Ord def merge_values_by_key(first: List[T], second: List[T], key_getter: Callable[[T], K], merge: Callable[[Optional[T], Optional[T]], T]) -> List[T]: + """ + Merge the values of two lists when the key matches. + Used in order to de-duplicate report entries depending on certain criteria, such as paths or feature names. + +>>> def merge_func(a, b): +... if a == None: +... return (b[0], b[1] + 100) +... if b == None: +... return (a[0], a[1] + 200) +... return (a[0], a[1] + b[1]) +>>> first = [('one', 1), ('two', 2)] +>>> second = [('two', 3), ('three', 4)] +>>> key_getter = lambda item: item[0] +>>> merge_values_by_key(first, second, key_getter, merge_func) +[('one', 201), ('two', 5), ('three', 104)] + """ first_as_dict = list_as_key_value_dict(first, key_getter) second_as_dict = list_as_key_value_dict(second, key_getter) union = OrderedDict.fromkeys(list(first_as_dict.keys()) + list(second_as_dict.keys())) From 873f6452a191d5bd7b95a7a9fec5af46c31402d1 Mon Sep 17 00:00:00 2001 From: Claudiu-Marcel Bruda Date: Mon, 28 Feb 2022 12:07:40 +0200 Subject: [PATCH 42/44] fix mypy.ini requests_cache --- mypy.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy.ini b/mypy.ini index 982e1f35..51078b91 100644 --- a/mypy.ini +++ b/mypy.ini @@ -24,5 +24,5 @@ ignore_missing_imports = True [mypy-semver.*] ignore_missing_imports = True -[mypy-requests-cache.*] +[mypy-requests_cache.*] ignore_missing_imports = True From 2341caf86783a1aaf28d104f68e85fd42b847820 Mon Sep 17 00:00:00 2001 From: Andrei Bancioiu Date: Tue, 1 Mar 2022 14:05:44 +0200 Subject: [PATCH 43/44] Version 1.1.0. --- erdpy/CHANGELOG.md | 9 +++++++++ erdpy/_version.py | 2 +- setup.py | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/erdpy/CHANGELOG.md b/erdpy/CHANGELOG.md index cdcfc2a0..25e4443a 100644 --- a/erdpy/CHANGELOG.md +++ b/erdpy/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes will be documented in this file. Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. +## [Unreleased] + - TBD + +## [1.1.0] - 01.03.2022 + - [Add reports: contract sizes and twiggy symbol checks](https://github.com/ElrondNetwork/elrond-sdk-erdpy/pull/106) + - [Add `--recursive` option on contract build](https://github.com/ElrondNetwork/elrond-sdk-erdpy/pull/104) + - [Local testnet: minor refactoring, and remove `*:TRACE` from the default log-level](https://github.com/ElrondNetwork/elrond-sdk-erdpy/pull/103) + - [Transaction simulation & cost simulation: fix & redesign](https://github.com/ElrondNetwork/elrond-sdk-erdpy/pull/102) + ## [1.0.25] - 02.02.2022 - Remove old & deprecated, experimental code related to the Elrond IDE - Fix `erdpy account get-transactions` (handle transactions with no data field) diff --git a/erdpy/_version.py b/erdpy/_version.py index 9b719b6e..6849410a 100644 --- a/erdpy/_version.py +++ b/erdpy/_version.py @@ -1 +1 @@ -__version__ = "1.0.25" +__version__ = "1.1.0" diff --git a/setup.py b/setup.py index 6132e3f4..1cb75958 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ with open("README.md", "r") as fh: long_description = "https://github.com/ElrondNetwork/elrond-sdk-erdpy" -VERSION = "1.0.25" +VERSION = "1.1.0" try: with open('./erdpy/_version.py', 'wt') as versionfile: From bf27ad675a6ec7e0da628262883a93197558fe7f Mon Sep 17 00:00:00 2001 From: Andrei Bancioiu Date: Tue, 1 Mar 2022 14:11:00 +0200 Subject: [PATCH 44/44] Update CLI.md. --- erdpy/CLI.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/erdpy/CLI.md b/erdpy/CLI.md index 9d4eb6bf..379f2c88 100644 --- a/erdpy/CLI.md +++ b/erdpy/CLI.md @@ -58,7 +58,7 @@ usage: erdpy contract COMMAND [-h] ... Build, deploy and interact with Smart Contracts COMMANDS: - {new,templates,build,clean,test,deploy,call,upgrade,query} + {new,templates,build,clean,test,report,deploy,call,upgrade,query} OPTIONS: -h, --help show this help message and exit @@ -71,6 +71,7 @@ templates List the available Smart Contract templates. build Build a Smart Contract project using the appropriate buildchain. clean Clean a Smart Contract project. test Run Mandos tests. +report Print a detailed report of the smart contracts. deploy Deploy a Smart Contract. call Interact with a Smart Contract (execute function). upgrade Upgrade a previously-deployed Smart Contract. @@ -122,6 +123,7 @@ positional arguments: optional arguments: -h, --help show this help message and exit + -r, --recursive locate projects recursively --debug set debug flag (default: False) --no-optimization bypass optimizations (for clang) (default: False) --no-wasm-opt do not optimize wasm files after the build (default: False) @@ -142,10 +144,11 @@ usage: erdpy contract clean [-h] ... Clean a Smart Contract project. positional arguments: - project 🗀 the project directory (default: current directory) + project 🗀 the project directory (default: current directory) optional arguments: - -h, --help show this help message and exit + -h, --help show this help message and exit + -r, --recursive locate projects recursively ``` ### Contract.Deploy @@ -1314,7 +1317,7 @@ usage: erdpy deps install [-h] ... Install dependencies or elrond-sdk modules. positional arguments: - {all,llvm,clang,cpp,rust,nodejs,golang,vmtools,elrond_go,elrond_proxy_go,mcl_signer,wasm-opt} + {all,llvm,clang,cpp,rust,nodejs,golang,vmtools,elrond_go,elrond_proxy_go,mcl_signer,wasm-opt,twiggy} the dependency to install optional arguments: @@ -1333,7 +1336,7 @@ usage: erdpy deps check [-h] ... Check whether a dependency is installed. positional arguments: - {all,llvm,clang,cpp,rust,nodejs,golang,vmtools,elrond_go,elrond_proxy_go,mcl_signer,wasm-opt} + {all,llvm,clang,cpp,rust,nodejs,golang,vmtools,elrond_go,elrond_proxy_go,mcl_signer,wasm-opt,twiggy} the dependency to check optional arguments: