mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-02-16 17:42:02 +00:00
chore(common): add type hints to some coin tools scripts
Type-checking tools are still complaining at some places about inconsistencies. It would be too much effort to make them completely happy.
This commit is contained in:
parent
5671bd037b
commit
519f79f9eb
@ -1,10 +1,13 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from collections import OrderedDict, defaultdict
|
from collections import OrderedDict, defaultdict
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any, Callable, Iterable, Iterator, Literal, TypedDict, cast
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import requests
|
import requests
|
||||||
@ -21,7 +24,125 @@ else:
|
|||||||
DEFS_DIR = ROOT / "defs"
|
DEFS_DIR = ROOT / "defs"
|
||||||
|
|
||||||
|
|
||||||
def load_json(*path):
|
class SupportItemBool(TypedDict):
|
||||||
|
supported: dict[str, bool]
|
||||||
|
unsupported: dict[str, bool]
|
||||||
|
|
||||||
|
|
||||||
|
class SupportItemVersion(TypedDict):
|
||||||
|
supported: dict[str, str]
|
||||||
|
unsupported: dict[str, str]
|
||||||
|
|
||||||
|
|
||||||
|
class SupportData(TypedDict):
|
||||||
|
connect: SupportItemBool
|
||||||
|
suite: SupportItemBool
|
||||||
|
trezor1: SupportItemVersion
|
||||||
|
trezor2: SupportItemVersion
|
||||||
|
|
||||||
|
|
||||||
|
class SupportInfoItem(TypedDict):
|
||||||
|
connect: bool
|
||||||
|
suite: bool
|
||||||
|
trezor1: Literal[False] | str
|
||||||
|
trezor2: Literal[False] | str
|
||||||
|
|
||||||
|
|
||||||
|
SupportInfo = dict[str, SupportInfoItem]
|
||||||
|
|
||||||
|
|
||||||
|
class Coin(TypedDict):
|
||||||
|
# Necessary fields for BTC - from BTC_CHECKS
|
||||||
|
coin_name: str
|
||||||
|
coin_shortcut: str
|
||||||
|
coin_label: str
|
||||||
|
website: str
|
||||||
|
github: str
|
||||||
|
maintainer: str
|
||||||
|
curve_name: str
|
||||||
|
address_type: int
|
||||||
|
address_type_p2sh: int
|
||||||
|
maxfee_kb: int
|
||||||
|
minfee_kb: int
|
||||||
|
hash_genesis_block: str
|
||||||
|
xprv_magic: int
|
||||||
|
xpub_magic: int
|
||||||
|
xpub_magic_segwit_p2sh: int
|
||||||
|
xpub_magic_segwit_native: int
|
||||||
|
slip44: int
|
||||||
|
segwit: bool
|
||||||
|
decred: bool
|
||||||
|
fork_id: int
|
||||||
|
force_bip143: bool
|
||||||
|
default_fee_b: dict[str, int]
|
||||||
|
dust_limit: int
|
||||||
|
blocktime_seconds: int
|
||||||
|
signed_message_header: str
|
||||||
|
uri_prefix: str
|
||||||
|
min_address_length: int
|
||||||
|
max_address_length: int
|
||||||
|
bech32_prefix: str
|
||||||
|
cashaddr_prefix: str
|
||||||
|
|
||||||
|
# Other fields optionally coming from JSON
|
||||||
|
links: dict[str, str]
|
||||||
|
wallet: dict[str, str]
|
||||||
|
curve: str
|
||||||
|
decimals: int
|
||||||
|
|
||||||
|
# Mandatory fields added later in coin.update()
|
||||||
|
name: str
|
||||||
|
shortcut: str
|
||||||
|
key: str
|
||||||
|
icon: str
|
||||||
|
|
||||||
|
# Special ETH fields
|
||||||
|
chain: str
|
||||||
|
chain_id: str
|
||||||
|
rskip60: bool
|
||||||
|
url: str
|
||||||
|
|
||||||
|
# Special erc20 fields
|
||||||
|
symbol: str
|
||||||
|
address: str
|
||||||
|
address_bytes: bytes
|
||||||
|
dup_key_nontoken: bool
|
||||||
|
deprecation: dict[str, str]
|
||||||
|
|
||||||
|
# Special NEM fields
|
||||||
|
ticker: str
|
||||||
|
|
||||||
|
# Fields that are being created
|
||||||
|
unsupported: bool
|
||||||
|
duplicate: bool
|
||||||
|
support: SupportInfoItem
|
||||||
|
|
||||||
|
# Backend-oriented fields
|
||||||
|
blockchain_link: dict[str, Any]
|
||||||
|
blockbook: list[str]
|
||||||
|
bitcore: list[str]
|
||||||
|
|
||||||
|
|
||||||
|
Coins = list[Coin]
|
||||||
|
CoinBuckets = dict[str, Coins]
|
||||||
|
|
||||||
|
|
||||||
|
class FidoApp(TypedDict):
|
||||||
|
name: str
|
||||||
|
webauthn: list[str]
|
||||||
|
u2f: list[dict[str, str]]
|
||||||
|
use_sign_count: bool
|
||||||
|
use_self_attestation: bool
|
||||||
|
no_icon: bool
|
||||||
|
|
||||||
|
key: str
|
||||||
|
icon: str
|
||||||
|
|
||||||
|
|
||||||
|
FidoApps = list[FidoApp]
|
||||||
|
|
||||||
|
|
||||||
|
def load_json(*path: str | Path) -> Any:
|
||||||
"""Convenience function to load a JSON file from DEFS_DIR."""
|
"""Convenience function to load a JSON file from DEFS_DIR."""
|
||||||
if len(path) == 1 and isinstance(path[0], Path):
|
if len(path) == 1 and isinstance(path[0], Path):
|
||||||
file = path[0]
|
file = path[0]
|
||||||
@ -34,7 +155,7 @@ def load_json(*path):
|
|||||||
# ====== CoinsInfo ======
|
# ====== CoinsInfo ======
|
||||||
|
|
||||||
|
|
||||||
class CoinsInfo(dict):
|
class CoinsInfo(dict[str, Coins]):
|
||||||
"""Collection of information about all known kinds of coins.
|
"""Collection of information about all known kinds of coins.
|
||||||
|
|
||||||
It contains the following lists:
|
It contains the following lists:
|
||||||
@ -47,13 +168,13 @@ class CoinsInfo(dict):
|
|||||||
Accessible as a dict or by attribute: `info["misc"] == info.misc`
|
Accessible as a dict or by attribute: `info["misc"] == info.misc`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def as_list(self):
|
def as_list(self) -> Coins:
|
||||||
return sum(self.values(), [])
|
return sum(self.values(), [])
|
||||||
|
|
||||||
def as_dict(self):
|
def as_dict(self) -> dict[str, Coin]:
|
||||||
return {coin["key"]: coin for coin in self.as_list()}
|
return {coin["key"]: coin for coin in self.as_list()}
|
||||||
|
|
||||||
def __getattr__(self, attr):
|
def __getattr__(self, attr: str) -> Coins:
|
||||||
if attr in self:
|
if attr in self:
|
||||||
return self[attr]
|
return self[attr]
|
||||||
else:
|
else:
|
||||||
@ -63,7 +184,14 @@ class CoinsInfo(dict):
|
|||||||
# ====== coin validation ======
|
# ====== coin validation ======
|
||||||
|
|
||||||
|
|
||||||
def check_type(val, types, nullable=False, empty=False, regex=None, choice=None):
|
def check_type(
|
||||||
|
val: Any,
|
||||||
|
types: type | tuple[type, ...],
|
||||||
|
nullable: bool = False,
|
||||||
|
empty: bool = False,
|
||||||
|
regex: str | None = None,
|
||||||
|
choice: list[str] | None = None,
|
||||||
|
) -> None:
|
||||||
# check nullable
|
# check nullable
|
||||||
if val is None:
|
if val is None:
|
||||||
if nullable:
|
if nullable:
|
||||||
@ -85,6 +213,7 @@ def check_type(val, types, nullable=False, empty=False, regex=None, choice=None)
|
|||||||
if regex is not None:
|
if regex is not None:
|
||||||
if types is not str:
|
if types is not str:
|
||||||
raise TypeError("Wrong type for regex check")
|
raise TypeError("Wrong type for regex check")
|
||||||
|
assert isinstance(val, str)
|
||||||
if not re.search(regex, val):
|
if not re.search(regex, val):
|
||||||
raise ValueError(f"Value does not match regex {regex}")
|
raise ValueError(f"Value does not match regex {regex}")
|
||||||
|
|
||||||
@ -94,8 +223,10 @@ def check_type(val, types, nullable=False, empty=False, regex=None, choice=None)
|
|||||||
raise ValueError(f"Value not allowed, use one of: {choice_str}")
|
raise ValueError(f"Value not allowed, use one of: {choice_str}")
|
||||||
|
|
||||||
|
|
||||||
def check_key(key, types, optional=False, **kwargs):
|
def check_key(
|
||||||
def do_check(coin):
|
key: str, types: type | tuple[type, ...], optional: bool = False, **kwargs: Any
|
||||||
|
) -> Callable[[Coin], None]:
|
||||||
|
def do_check(coin: Coin) -> None:
|
||||||
if key not in coin:
|
if key not in coin:
|
||||||
if optional:
|
if optional:
|
||||||
return
|
return
|
||||||
@ -152,15 +283,15 @@ BTC_CHECKS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def validate_btc(coin):
|
def validate_btc(coin: Coin) -> list[str]:
|
||||||
errors = []
|
errors: list[str] = []
|
||||||
for check in BTC_CHECKS:
|
for check in BTC_CHECKS:
|
||||||
try:
|
try:
|
||||||
check(coin)
|
check(coin)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
errors.append(str(e))
|
errors.append(str(e))
|
||||||
|
|
||||||
magics = [
|
magics: list[int] = [
|
||||||
coin[k]
|
coin[k]
|
||||||
for k in (
|
for k in (
|
||||||
"xprv_magic",
|
"xprv_magic",
|
||||||
@ -208,11 +339,11 @@ def validate_btc(coin):
|
|||||||
# ======= Coin json loaders =======
|
# ======= Coin json loaders =======
|
||||||
|
|
||||||
|
|
||||||
def _load_btc_coins():
|
def _load_btc_coins() -> Coins:
|
||||||
"""Load btc-like coins from `bitcoin/*.json`"""
|
"""Load btc-like coins from `bitcoin/*.json`"""
|
||||||
coins = []
|
coins: Coins = []
|
||||||
for file in DEFS_DIR.glob("bitcoin/*.json"):
|
for file in DEFS_DIR.glob("bitcoin/*.json"):
|
||||||
coin = load_json(file)
|
coin: Coin = load_json(file)
|
||||||
coin.update(
|
coin.update(
|
||||||
name=coin["coin_label"],
|
name=coin["coin_label"],
|
||||||
shortcut=coin["coin_shortcut"],
|
shortcut=coin["coin_shortcut"],
|
||||||
@ -224,10 +355,10 @@ def _load_btc_coins():
|
|||||||
return coins
|
return coins
|
||||||
|
|
||||||
|
|
||||||
def _load_ethereum_networks():
|
def _load_ethereum_networks() -> Coins:
|
||||||
"""Load ethereum networks from `ethereum/networks.json`"""
|
"""Load ethereum networks from `ethereum/networks.json`"""
|
||||||
chains_path = DEFS_DIR / "ethereum" / "chains" / "_data" / "chains"
|
chains_path = DEFS_DIR / "ethereum" / "chains" / "_data" / "chains"
|
||||||
networks = []
|
networks: Coins = []
|
||||||
for chain in sorted(
|
for chain in sorted(
|
||||||
chains_path.glob("eip155-*.json"),
|
chains_path.glob("eip155-*.json"),
|
||||||
key=lambda x: int(x.stem.replace("eip155-", "")),
|
key=lambda x: int(x.stem.replace("eip155-", "")),
|
||||||
@ -261,21 +392,21 @@ def _load_ethereum_networks():
|
|||||||
url=chain_data["infoURL"],
|
url=chain_data["infoURL"],
|
||||||
key=f"eth:{shortcut}",
|
key=f"eth:{shortcut}",
|
||||||
)
|
)
|
||||||
networks.append(network)
|
networks.append(cast(Coin, network))
|
||||||
|
|
||||||
return networks
|
return networks
|
||||||
|
|
||||||
|
|
||||||
def _load_erc20_tokens():
|
def _load_erc20_tokens() -> Coins:
|
||||||
"""Load ERC20 tokens from `ethereum/tokens` submodule."""
|
"""Load ERC20 tokens from `ethereum/tokens` submodule."""
|
||||||
networks = _load_ethereum_networks()
|
networks = _load_ethereum_networks()
|
||||||
tokens = []
|
tokens: Coins = []
|
||||||
for network in networks:
|
for network in networks:
|
||||||
chain = network["chain"]
|
chain = network["chain"]
|
||||||
|
|
||||||
chain_path = DEFS_DIR / "ethereum" / "tokens" / "tokens" / chain
|
chain_path = DEFS_DIR / "ethereum" / "tokens" / "tokens" / chain
|
||||||
for file in sorted(chain_path.glob("*.json")):
|
for file in sorted(chain_path.glob("*.json")):
|
||||||
token = load_json(file)
|
token: Coin = load_json(file)
|
||||||
token.update(
|
token.update(
|
||||||
chain=chain,
|
chain=chain,
|
||||||
chain_id=network["chain_id"],
|
chain_id=network["chain_id"],
|
||||||
@ -288,26 +419,26 @@ def _load_erc20_tokens():
|
|||||||
return tokens
|
return tokens
|
||||||
|
|
||||||
|
|
||||||
def _load_nem_mosaics():
|
def _load_nem_mosaics() -> Coins:
|
||||||
"""Loads NEM mosaics from `nem/nem_mosaics.json`"""
|
"""Loads NEM mosaics from `nem/nem_mosaics.json`"""
|
||||||
mosaics = load_json("nem/nem_mosaics.json")
|
mosaics: Coins = load_json("nem/nem_mosaics.json")
|
||||||
for mosaic in mosaics:
|
for mosaic in mosaics:
|
||||||
shortcut = mosaic["ticker"].strip()
|
shortcut = mosaic["ticker"].strip()
|
||||||
mosaic.update(shortcut=shortcut, key=f"nem:{shortcut}")
|
mosaic.update(shortcut=shortcut, key=f"nem:{shortcut}")
|
||||||
return mosaics
|
return mosaics
|
||||||
|
|
||||||
|
|
||||||
def _load_misc():
|
def _load_misc() -> Coins:
|
||||||
"""Loads miscellaneous networks from `misc/misc.json`"""
|
"""Loads miscellaneous networks from `misc/misc.json`"""
|
||||||
others = load_json("misc/misc.json")
|
others: Coins = load_json("misc/misc.json")
|
||||||
for other in others:
|
for other in others:
|
||||||
other.update(key=f"misc:{other['shortcut']}")
|
other.update(key=f"misc:{other['shortcut']}")
|
||||||
return others
|
return others
|
||||||
|
|
||||||
|
|
||||||
def _load_fido_apps():
|
def _load_fido_apps() -> FidoApps:
|
||||||
"""Load FIDO apps from `fido/*.json`"""
|
"""Load FIDO apps from `fido/*.json`"""
|
||||||
apps = []
|
apps: FidoApps = []
|
||||||
for file in sorted(DEFS_DIR.glob("fido/*.json")):
|
for file in sorted(DEFS_DIR.glob("fido/*.json")):
|
||||||
app_name = file.stem.lower()
|
app_name = file.stem.lower()
|
||||||
app = load_json(file)
|
app = load_json(file)
|
||||||
@ -335,28 +466,28 @@ MISSING_SUPPORT_MEANS_NO = ("connect", "suite")
|
|||||||
VERSIONED_SUPPORT_INFO = ("trezor1", "trezor2")
|
VERSIONED_SUPPORT_INFO = ("trezor1", "trezor2")
|
||||||
|
|
||||||
|
|
||||||
def get_support_data():
|
def get_support_data() -> SupportData:
|
||||||
"""Get raw support data from `support.json`."""
|
"""Get raw support data from `support.json`."""
|
||||||
return load_json("support.json")
|
return load_json("support.json")
|
||||||
|
|
||||||
|
|
||||||
def latest_releases():
|
def latest_releases() -> dict[str, Any]:
|
||||||
"""Get latest released firmware versions for Trezor 1 and 2"""
|
"""Get latest released firmware versions for Trezor 1 and 2"""
|
||||||
if not requests:
|
if not requests:
|
||||||
raise RuntimeError("requests library is required for getting release info")
|
raise RuntimeError("requests library is required for getting release info")
|
||||||
|
|
||||||
latest = {}
|
latest: dict[str, Any] = {}
|
||||||
for v in ("1", "2"):
|
for v in ("1", "2"):
|
||||||
releases = requests.get(RELEASES_URL.format(v)).json()
|
releases = requests.get(RELEASES_URL.format(v)).json()
|
||||||
latest["trezor" + v] = max(tuple(r["version"]) for r in releases)
|
latest["trezor" + v] = max(tuple(r["version"]) for r in releases)
|
||||||
return latest
|
return latest
|
||||||
|
|
||||||
|
|
||||||
def is_token(coin):
|
def is_token(coin: Coin) -> bool:
|
||||||
return coin["key"].startswith("erc20:")
|
return coin["key"].startswith("erc20:")
|
||||||
|
|
||||||
|
|
||||||
def support_info_single(support_data, coin):
|
def support_info_single(support_data: SupportData, coin: Coin) -> SupportInfoItem:
|
||||||
"""Extract a support dict from `support.json` data.
|
"""Extract a support dict from `support.json` data.
|
||||||
|
|
||||||
Returns a dict of support values for each "device", i.e., `support.json`
|
Returns a dict of support values for each "device", i.e., `support.json`
|
||||||
@ -368,22 +499,23 @@ def support_info_single(support_data, coin):
|
|||||||
(usually a version string, or `True` for connect/suite)
|
(usually a version string, or `True` for connect/suite)
|
||||||
* if the coin doesn't have an entry, its support status is `None`
|
* if the coin doesn't have an entry, its support status is `None`
|
||||||
"""
|
"""
|
||||||
support_info = {}
|
support_info_item = {}
|
||||||
key = coin["key"]
|
key = coin["key"]
|
||||||
for device, values in support_data.items():
|
for device, values in support_data.items():
|
||||||
|
assert isinstance(values, dict)
|
||||||
if key in values["unsupported"]:
|
if key in values["unsupported"]:
|
||||||
support_value = False
|
support_value: Any = False
|
||||||
elif key in values["supported"]:
|
elif key in values["supported"]:
|
||||||
support_value = values["supported"][key]
|
support_value = values["supported"][key]
|
||||||
elif device in MISSING_SUPPORT_MEANS_NO:
|
elif device in MISSING_SUPPORT_MEANS_NO:
|
||||||
support_value = False
|
support_value = False
|
||||||
else:
|
else:
|
||||||
support_value = None
|
support_value = None
|
||||||
support_info[device] = support_value
|
support_info_item[device] = support_value
|
||||||
return support_info
|
return cast(SupportInfoItem, support_info_item)
|
||||||
|
|
||||||
|
|
||||||
def support_info(coins):
|
def support_info(coins: Iterable[Coin] | CoinsInfo | dict[str, Coin]) -> SupportInfo:
|
||||||
"""Generate Trezor support information.
|
"""Generate Trezor support information.
|
||||||
|
|
||||||
Takes a collection of coins and generates a support-info entry for each.
|
Takes a collection of coins and generates a support-info entry for each.
|
||||||
@ -401,7 +533,7 @@ def support_info(coins):
|
|||||||
coins = coins.values()
|
coins = coins.values()
|
||||||
|
|
||||||
support_data = get_support_data()
|
support_data = get_support_data()
|
||||||
support = {}
|
support: SupportInfo = {}
|
||||||
for coin in coins:
|
for coin in coins:
|
||||||
support[coin["key"]] = support_info_single(support_data, coin)
|
support[coin["key"]] = support_info_single(support_data, coin)
|
||||||
|
|
||||||
@ -411,19 +543,19 @@ def support_info(coins):
|
|||||||
# ====== data cleanup functions ======
|
# ====== data cleanup functions ======
|
||||||
|
|
||||||
|
|
||||||
def _ensure_mandatory_values(coins):
|
def _ensure_mandatory_values(coins: Coins) -> None:
|
||||||
"""Checks that every coin has the mandatory fields: name, shortcut, key"""
|
"""Checks that every coin has the mandatory fields: name, shortcut, key"""
|
||||||
for coin in coins:
|
for coin in coins:
|
||||||
if not all(coin.get(k) for k in ("name", "shortcut", "key")):
|
if not all(coin.get(k) for k in ("name", "shortcut", "key")):
|
||||||
raise ValueError(coin)
|
raise ValueError(coin)
|
||||||
|
|
||||||
|
|
||||||
def symbol_from_shortcut(shortcut):
|
def symbol_from_shortcut(shortcut: str) -> tuple[str, str]:
|
||||||
symsplit = shortcut.split(" ", maxsplit=1)
|
symsplit = shortcut.split(" ", maxsplit=1)
|
||||||
return symsplit[0], symsplit[1] if len(symsplit) > 1 else ""
|
return symsplit[0], symsplit[1] if len(symsplit) > 1 else ""
|
||||||
|
|
||||||
|
|
||||||
def mark_duplicate_shortcuts(coins):
|
def mark_duplicate_shortcuts(coins: Coins) -> CoinBuckets:
|
||||||
"""Finds coins with identical symbols and sets their `duplicate` field.
|
"""Finds coins with identical symbols and sets their `duplicate` field.
|
||||||
|
|
||||||
"Symbol" here means the first part of `shortcut` (separated by space),
|
"Symbol" here means the first part of `shortcut` (separated by space),
|
||||||
@ -436,7 +568,7 @@ def mark_duplicate_shortcuts(coins):
|
|||||||
Each coin in every bucket will have its "duplicate" property set to True, unless
|
Each coin in every bucket will have its "duplicate" property set to True, unless
|
||||||
it's explicitly marked as `false` in `duplicity_overrides.json`.
|
it's explicitly marked as `false` in `duplicity_overrides.json`.
|
||||||
"""
|
"""
|
||||||
dup_symbols = defaultdict(list)
|
dup_symbols: CoinBuckets = defaultdict(list)
|
||||||
|
|
||||||
for coin in coins:
|
for coin in coins:
|
||||||
symbol, _ = symbol_from_shortcut(coin["shortcut"].lower())
|
symbol, _ = symbol_from_shortcut(coin["shortcut"].lower())
|
||||||
@ -451,9 +583,9 @@ def mark_duplicate_shortcuts(coins):
|
|||||||
return dup_symbols
|
return dup_symbols
|
||||||
|
|
||||||
|
|
||||||
def apply_duplicity_overrides(coins):
|
def apply_duplicity_overrides(coins: Coins) -> Coins:
|
||||||
overrides = load_json("duplicity_overrides.json")
|
overrides = load_json("duplicity_overrides.json")
|
||||||
override_bucket = []
|
override_bucket: Coins = []
|
||||||
for coin in coins:
|
for coin in coins:
|
||||||
override_value = overrides.get(coin["key"])
|
override_value = overrides.get(coin["key"])
|
||||||
if override_value is True:
|
if override_value is True:
|
||||||
@ -464,7 +596,7 @@ def apply_duplicity_overrides(coins):
|
|||||||
return override_bucket
|
return override_bucket
|
||||||
|
|
||||||
|
|
||||||
def deduplicate_erc20(buckets, networks):
|
def deduplicate_erc20(buckets: CoinBuckets, networks: Coins) -> None:
|
||||||
"""Apply further processing to ERC20 duplicate buckets.
|
"""Apply further processing to ERC20 duplicate buckets.
|
||||||
|
|
||||||
This function works on results of `mark_duplicate_shortcuts`.
|
This function works on results of `mark_duplicate_shortcuts`.
|
||||||
@ -489,7 +621,7 @@ def deduplicate_erc20(buckets, networks):
|
|||||||
|
|
||||||
testnet_networks = {n["chain"] for n in networks if "Testnet" in n["name"]}
|
testnet_networks = {n["chain"] for n in networks if "Testnet" in n["name"]}
|
||||||
|
|
||||||
def clear_bucket(bucket):
|
def clear_bucket(bucket: Coins) -> None:
|
||||||
# allow all coins, except those that are explicitly marked through overrides
|
# allow all coins, except those that are explicitly marked through overrides
|
||||||
for coin in bucket:
|
for coin in bucket:
|
||||||
coin["duplicate"] = False
|
coin["duplicate"] = False
|
||||||
@ -531,8 +663,8 @@ def deduplicate_erc20(buckets, networks):
|
|||||||
clear_bucket(bucket)
|
clear_bucket(bucket)
|
||||||
|
|
||||||
|
|
||||||
def deduplicate_keys(all_coins):
|
def deduplicate_keys(all_coins: Coins) -> None:
|
||||||
dups = defaultdict(list)
|
dups: CoinBuckets = defaultdict(list)
|
||||||
for coin in all_coins:
|
for coin in all_coins:
|
||||||
dups[coin["key"]].append(coin)
|
dups[coin["key"]].append(coin)
|
||||||
|
|
||||||
@ -549,7 +681,7 @@ def deduplicate_keys(all_coins):
|
|||||||
coin["dup_key_nontoken"] = True
|
coin["dup_key_nontoken"] = True
|
||||||
|
|
||||||
|
|
||||||
def fill_blockchain_links(all_coins):
|
def fill_blockchain_links(all_coins: CoinsInfo) -> None:
|
||||||
blockchain_links = load_json("blockchain_link.json")
|
blockchain_links = load_json("blockchain_link.json")
|
||||||
for coins in all_coins.values():
|
for coins in all_coins.values():
|
||||||
for coin in coins:
|
for coin in coins:
|
||||||
@ -561,14 +693,14 @@ def fill_blockchain_links(all_coins):
|
|||||||
coin["blockbook"] = []
|
coin["blockbook"] = []
|
||||||
|
|
||||||
|
|
||||||
def _btc_sort_key(coin):
|
def _btc_sort_key(coin: Coin) -> str:
|
||||||
if coin["name"] in ("Bitcoin", "Testnet", "Regtest"):
|
if coin["name"] in ("Bitcoin", "Testnet", "Regtest"):
|
||||||
return "000000" + coin["name"]
|
return "000000" + coin["name"]
|
||||||
else:
|
else:
|
||||||
return coin["name"]
|
return coin["name"]
|
||||||
|
|
||||||
|
|
||||||
def collect_coin_info():
|
def collect_coin_info() -> CoinsInfo:
|
||||||
"""Returns all definition as dict organized by coin type.
|
"""Returns all definition as dict organized by coin type.
|
||||||
`coins` for btc-like coins,
|
`coins` for btc-like coins,
|
||||||
`eth` for ethereum networks,
|
`eth` for ethereum networks,
|
||||||
@ -592,7 +724,7 @@ def collect_coin_info():
|
|||||||
return all_coins
|
return all_coins
|
||||||
|
|
||||||
|
|
||||||
def sort_coin_infos(all_coins):
|
def sort_coin_infos(all_coins: CoinsInfo) -> None:
|
||||||
for k, coins in all_coins.items():
|
for k, coins in all_coins.items():
|
||||||
if k == "bitcoin":
|
if k == "bitcoin":
|
||||||
coins.sort(key=_btc_sort_key)
|
coins.sort(key=_btc_sort_key)
|
||||||
@ -606,7 +738,7 @@ def sort_coin_infos(all_coins):
|
|||||||
coins.sort(key=lambda c: c["key"].upper())
|
coins.sort(key=lambda c: c["key"].upper())
|
||||||
|
|
||||||
|
|
||||||
def coin_info_with_duplicates():
|
def coin_info_with_duplicates() -> tuple[CoinsInfo, CoinBuckets]:
|
||||||
"""Collects coin info, detects duplicates but does not remove them.
|
"""Collects coin info, detects duplicates but does not remove them.
|
||||||
|
|
||||||
Returns the CoinsInfo object and duplicate buckets.
|
Returns the CoinsInfo object and duplicate buckets.
|
||||||
@ -626,7 +758,7 @@ def coin_info_with_duplicates():
|
|||||||
return all_coins, buckets
|
return all_coins, buckets
|
||||||
|
|
||||||
|
|
||||||
def coin_info():
|
def coin_info() -> CoinsInfo:
|
||||||
"""Collects coin info, fills out support info and returns the result.
|
"""Collects coin info, fills out support info and returns the result.
|
||||||
|
|
||||||
Does not auto-delete duplicates. This should now be based on support info.
|
Does not auto-delete duplicates. This should now be based on support info.
|
||||||
@ -638,12 +770,12 @@ def coin_info():
|
|||||||
return all_coins
|
return all_coins
|
||||||
|
|
||||||
|
|
||||||
def fido_info():
|
def fido_info() -> FidoApps:
|
||||||
"""Returns info about known FIDO/U2F apps."""
|
"""Returns info about known FIDO/U2F apps."""
|
||||||
return _load_fido_apps()
|
return _load_fido_apps()
|
||||||
|
|
||||||
|
|
||||||
def search(coins, keyword):
|
def search(coins: CoinsInfo | Coins, keyword: str) -> Iterator[Any]:
|
||||||
kwl = keyword.lower()
|
kwl = keyword.lower()
|
||||||
if isinstance(coins, CoinsInfo):
|
if isinstance(coins, CoinsInfo):
|
||||||
coins = coins.as_list()
|
coins = coins.as_list()
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import glob
|
import glob
|
||||||
import json
|
import json
|
||||||
@ -8,10 +10,12 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
|
from typing import Any, Callable, Iterator, TextIO, cast
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
import coin_info
|
import coin_info
|
||||||
|
from coin_info import Coin, CoinBuckets, Coins, CoinsInfo, FidoApps, SupportInfo
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import termcolor
|
import termcolor
|
||||||
@ -44,7 +48,9 @@ except ImportError:
|
|||||||
USE_COLORS = False
|
USE_COLORS = False
|
||||||
|
|
||||||
|
|
||||||
def crayon(color, string, bold=False, dim=False):
|
def crayon(
|
||||||
|
color: str | None, string: str, bold: bool = False, dim: bool = False
|
||||||
|
) -> str:
|
||||||
if not termcolor or not USE_COLORS:
|
if not termcolor or not USE_COLORS:
|
||||||
return string
|
return string
|
||||||
else:
|
else:
|
||||||
@ -57,7 +63,7 @@ def crayon(color, string, bold=False, dim=False):
|
|||||||
return termcolor.colored(string, color, attrs=attrs)
|
return termcolor.colored(string, color, attrs=attrs)
|
||||||
|
|
||||||
|
|
||||||
def print_log(level, *args, **kwargs):
|
def print_log(level: int, *args: Any, **kwargs: Any) -> None:
|
||||||
prefix = logging.getLevelName(level)
|
prefix = logging.getLevelName(level)
|
||||||
if level == logging.DEBUG:
|
if level == logging.DEBUG:
|
||||||
prefix = crayon("blue", prefix, bold=False)
|
prefix = crayon("blue", prefix, bold=False)
|
||||||
@ -73,11 +79,11 @@ def print_log(level, *args, **kwargs):
|
|||||||
# ======= Mako management ======
|
# ======= Mako management ======
|
||||||
|
|
||||||
|
|
||||||
def c_str_filter(b):
|
def c_str_filter(b: Any) -> str:
|
||||||
if b is None:
|
if b is None:
|
||||||
return "NULL"
|
return "NULL"
|
||||||
|
|
||||||
def hexescape(c):
|
def hexescape(c: bytes) -> str:
|
||||||
return rf"\x{c:02x}"
|
return rf"\x{c:02x}"
|
||||||
|
|
||||||
if isinstance(b, bytes):
|
if isinstance(b, bytes):
|
||||||
@ -86,7 +92,7 @@ def c_str_filter(b):
|
|||||||
return json.dumps(b)
|
return json.dumps(b)
|
||||||
|
|
||||||
|
|
||||||
def black_repr_filter(val):
|
def black_repr_filter(val: Any) -> str:
|
||||||
if isinstance(val, str):
|
if isinstance(val, str):
|
||||||
if '"' in val:
|
if '"' in val:
|
||||||
return repr(val)
|
return repr(val)
|
||||||
@ -98,12 +104,14 @@ def black_repr_filter(val):
|
|||||||
return repr(val)
|
return repr(val)
|
||||||
|
|
||||||
|
|
||||||
def ascii_filter(s):
|
def ascii_filter(s: str) -> str:
|
||||||
return re.sub("[^ -\x7e]", "_", s)
|
return re.sub("[^ -\x7e]", "_", s)
|
||||||
|
|
||||||
|
|
||||||
def make_support_filter(support_info):
|
def make_support_filter(
|
||||||
def supported_on(device, coins):
|
support_info: SupportInfo,
|
||||||
|
) -> Callable[[str, Coins], Iterator[Coin]]:
|
||||||
|
def supported_on(device: str, coins: Coins) -> Iterator[Coin]:
|
||||||
return (c for c in coins if support_info[c.key].get(device))
|
return (c for c in coins if support_info[c.key].get(device))
|
||||||
|
|
||||||
return supported_on
|
return supported_on
|
||||||
@ -116,7 +124,9 @@ MAKO_FILTERS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def render_file(src, dst, coins, support_info):
|
def render_file(
|
||||||
|
src: str, dst: TextIO, coins: CoinsInfo, support_info: SupportInfo
|
||||||
|
) -> None:
|
||||||
"""Renders `src` template into `dst`.
|
"""Renders `src` template into `dst`.
|
||||||
|
|
||||||
`src` is a filename, `dst` is an open file object.
|
`src` is a filename, `dst` is an open file object.
|
||||||
@ -134,14 +144,14 @@ def render_file(src, dst, coins, support_info):
|
|||||||
# ====== validation functions ======
|
# ====== validation functions ======
|
||||||
|
|
||||||
|
|
||||||
def mark_unsupported(support_info, coins):
|
def mark_unsupported(support_info: SupportInfo, coins: Coins) -> None:
|
||||||
for coin in coins:
|
for coin in coins:
|
||||||
key = coin["key"]
|
key = coin["key"]
|
||||||
# checking for explicit False because None means unknown
|
# checking for explicit False because None means unknown
|
||||||
coin["unsupported"] = all(v is False for v in support_info[key].values())
|
coin["unsupported"] = all(v is False for v in support_info[key].values())
|
||||||
|
|
||||||
|
|
||||||
def highlight_key(coin, color):
|
def highlight_key(coin: Coin, color: str) -> str:
|
||||||
"""Return a colorful string where the SYMBOL part is bold."""
|
"""Return a colorful string where the SYMBOL part is bold."""
|
||||||
keylist = coin["key"].split(":")
|
keylist = coin["key"].split(":")
|
||||||
if keylist[-1].isdigit():
|
if keylist[-1].isdigit():
|
||||||
@ -153,9 +163,9 @@ def highlight_key(coin, color):
|
|||||||
return f"{key} {name}"
|
return f"{key} {name}"
|
||||||
|
|
||||||
|
|
||||||
def find_collisions(coins, field):
|
def find_collisions(coins: Coins, field: str) -> CoinBuckets:
|
||||||
"""Detects collisions in a given field. Returns buckets of colliding coins."""
|
"""Detects collisions in a given field. Returns buckets of colliding coins."""
|
||||||
collisions = defaultdict(list)
|
collisions: CoinBuckets = defaultdict(list)
|
||||||
for coin in coins:
|
for coin in coins:
|
||||||
values = coin[field]
|
values = coin[field]
|
||||||
if not isinstance(values, list):
|
if not isinstance(values, list):
|
||||||
@ -165,7 +175,7 @@ def find_collisions(coins, field):
|
|||||||
return {k: v for k, v in collisions.items() if len(v) > 1}
|
return {k: v for k, v in collisions.items() if len(v) > 1}
|
||||||
|
|
||||||
|
|
||||||
def check_eth(coins):
|
def check_eth(coins: Coins) -> bool:
|
||||||
check_passed = True
|
check_passed = True
|
||||||
chains = find_collisions(coins, "chain")
|
chains = find_collisions(coins, "chain")
|
||||||
for key, bucket in chains.items():
|
for key, bucket in chains.items():
|
||||||
@ -176,7 +186,7 @@ def check_eth(coins):
|
|||||||
return check_passed
|
return check_passed
|
||||||
|
|
||||||
|
|
||||||
def check_btc(coins):
|
def check_btc(coins: Coins) -> bool:
|
||||||
check_passed = True
|
check_passed = True
|
||||||
|
|
||||||
# validate individual coin data
|
# validate individual coin data
|
||||||
@ -187,9 +197,9 @@ def check_btc(coins):
|
|||||||
print_log(logging.ERROR, "invalid definition for", coin["name"])
|
print_log(logging.ERROR, "invalid definition for", coin["name"])
|
||||||
print("\n".join(errors))
|
print("\n".join(errors))
|
||||||
|
|
||||||
def collision_str(bucket):
|
def collision_str(bucket: Coins) -> str:
|
||||||
"""Generate a colorful string out of a bucket of colliding coins."""
|
"""Generate a colorful string out of a bucket of colliding coins."""
|
||||||
coin_strings = []
|
coin_strings: list[str] = []
|
||||||
for coin in bucket:
|
for coin in bucket:
|
||||||
name = coin["name"]
|
name = coin["name"]
|
||||||
prefix = ""
|
prefix = ""
|
||||||
@ -206,7 +216,12 @@ def check_btc(coins):
|
|||||||
coin_strings.append(prefix + hl)
|
coin_strings.append(prefix + hl)
|
||||||
return ", ".join(coin_strings)
|
return ", ".join(coin_strings)
|
||||||
|
|
||||||
def print_collision_buckets(buckets, prefix, maxlevel=logging.ERROR, strict=False):
|
def print_collision_buckets(
|
||||||
|
buckets: CoinBuckets,
|
||||||
|
prefix: str,
|
||||||
|
maxlevel: int = logging.ERROR,
|
||||||
|
strict: bool = False,
|
||||||
|
) -> bool:
|
||||||
"""Intelligently print collision buckets.
|
"""Intelligently print collision buckets.
|
||||||
|
|
||||||
For each bucket, if there are any collision with a mainnet, print it.
|
For each bucket, if there are any collision with a mainnet, print it.
|
||||||
@ -268,7 +283,7 @@ def check_btc(coins):
|
|||||||
return check_passed
|
return check_passed
|
||||||
|
|
||||||
|
|
||||||
def check_dups(buckets, print_at_level=logging.WARNING):
|
def check_dups(buckets: CoinBuckets, print_at_level: int = logging.WARNING) -> bool:
|
||||||
"""Analyze and pretty-print results of `coin_info.mark_duplicate_shortcuts`.
|
"""Analyze and pretty-print results of `coin_info.mark_duplicate_shortcuts`.
|
||||||
|
|
||||||
`print_at_level` can be one of logging levels.
|
`print_at_level` can be one of logging levels.
|
||||||
@ -279,7 +294,7 @@ def check_dups(buckets, print_at_level=logging.WARNING):
|
|||||||
If the collision includes more than one non-token, it's ERROR and printed always.
|
If the collision includes more than one non-token, it's ERROR and printed always.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def coin_str(coin):
|
def coin_str(coin: Coin) -> str:
|
||||||
"""Colorize coins. Tokens are cyan, nontokens are red. Coins that are NOT
|
"""Colorize coins. Tokens are cyan, nontokens are red. Coins that are NOT
|
||||||
marked duplicate get a green asterisk.
|
marked duplicate get a green asterisk.
|
||||||
"""
|
"""
|
||||||
@ -347,7 +362,7 @@ def check_dups(buckets, print_at_level=logging.WARNING):
|
|||||||
return check_passed
|
return check_passed
|
||||||
|
|
||||||
|
|
||||||
def check_backends(coins):
|
def check_backends(coins: Coins) -> bool:
|
||||||
check_passed = True
|
check_passed = True
|
||||||
for coin in coins:
|
for coin in coins:
|
||||||
genesis_block = coin.get("hash_genesis_block")
|
genesis_block = coin.get("hash_genesis_block")
|
||||||
@ -357,6 +372,7 @@ def check_backends(coins):
|
|||||||
for backend in backends:
|
for backend in backends:
|
||||||
print("checking", backend, "... ", end="", flush=True)
|
print("checking", backend, "... ", end="", flush=True)
|
||||||
try:
|
try:
|
||||||
|
assert requests is not None
|
||||||
j = requests.get(backend + "/api/block-index/0").json()
|
j = requests.get(backend + "/api/block-index/0").json()
|
||||||
if j["blockHash"] != genesis_block:
|
if j["blockHash"] != genesis_block:
|
||||||
raise RuntimeError("genesis block mismatch")
|
raise RuntimeError("genesis block mismatch")
|
||||||
@ -368,7 +384,7 @@ def check_backends(coins):
|
|||||||
return check_passed
|
return check_passed
|
||||||
|
|
||||||
|
|
||||||
def check_icons(coins):
|
def check_icons(coins: Coins) -> bool:
|
||||||
check_passed = True
|
check_passed = True
|
||||||
for coin in coins:
|
for coin in coins:
|
||||||
key = coin["key"]
|
key = coin["key"]
|
||||||
@ -394,8 +410,8 @@ def check_icons(coins):
|
|||||||
IGNORE_NONUNIFORM_KEYS = frozenset(("unsupported", "duplicate"))
|
IGNORE_NONUNIFORM_KEYS = frozenset(("unsupported", "duplicate"))
|
||||||
|
|
||||||
|
|
||||||
def check_key_uniformity(coins):
|
def check_key_uniformity(coins: Coins) -> bool:
|
||||||
keysets = defaultdict(list)
|
keysets: dict[frozenset[str], Coins] = defaultdict(list)
|
||||||
for coin in coins:
|
for coin in coins:
|
||||||
keyset = frozenset(coin.keys()) | IGNORE_NONUNIFORM_KEYS
|
keyset = frozenset(coin.keys()) | IGNORE_NONUNIFORM_KEYS
|
||||||
keysets[keyset].append(coin)
|
keysets[keyset].append(coin)
|
||||||
@ -426,7 +442,7 @@ def check_key_uniformity(coins):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def check_segwit(coins):
|
def check_segwit(coins: Coins) -> bool:
|
||||||
for coin in coins:
|
for coin in coins:
|
||||||
segwit = coin["segwit"]
|
segwit = coin["segwit"]
|
||||||
segwit_fields = [
|
segwit_fields = [
|
||||||
@ -471,7 +487,7 @@ FIDO_KNOWN_KEYS = frozenset(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def check_fido(apps):
|
def check_fido(apps: FidoApps) -> bool:
|
||||||
check_passed = True
|
check_passed = True
|
||||||
|
|
||||||
u2fs = find_collisions((u for a in apps if "u2f" in a for u in a["u2f"]), "app_id")
|
u2fs = find_collisions((u for a in apps if "u2f" in a for u in a["u2f"]), "app_id")
|
||||||
@ -488,7 +504,7 @@ def check_fido(apps):
|
|||||||
print_log(logging.ERROR, webauthn_str, bucket_str)
|
print_log(logging.ERROR, webauthn_str, bucket_str)
|
||||||
check_passed = False
|
check_passed = False
|
||||||
|
|
||||||
domain_hashes = {}
|
domain_hashes: dict[bytes, str] = {}
|
||||||
for app in apps:
|
for app in apps:
|
||||||
if "webauthn" in app:
|
if "webauthn" in app:
|
||||||
for domain in app["webauthn"]:
|
for domain in app["webauthn"]:
|
||||||
@ -574,7 +590,7 @@ def check_fido(apps):
|
|||||||
default=sys.stdout.isatty(),
|
default=sys.stdout.isatty(),
|
||||||
help="Force colored output on/off",
|
help="Force colored output on/off",
|
||||||
)
|
)
|
||||||
def cli(colors):
|
def cli(colors: bool) -> None:
|
||||||
global USE_COLORS
|
global USE_COLORS
|
||||||
USE_COLORS = colors
|
USE_COLORS = colors
|
||||||
|
|
||||||
@ -585,7 +601,7 @@ def cli(colors):
|
|||||||
@click.option("--icons/--no-icons", default=True, help="Check icon files")
|
@click.option("--icons/--no-icons", default=True, help="Check icon files")
|
||||||
@click.option("-d", "--show-duplicates", type=click.Choice(("all", "nontoken", "errors")), default="errors", help="How much information about duplicate shortcuts should be shown.")
|
@click.option("-d", "--show-duplicates", type=click.Choice(("all", "nontoken", "errors")), default="errors", help="How much information about duplicate shortcuts should be shown.")
|
||||||
# fmt: on
|
# fmt: on
|
||||||
def check(backend, icons, show_duplicates):
|
def check(backend: bool, icons: bool, show_duplicates: str) -> None:
|
||||||
"""Validate coin definitions.
|
"""Validate coin definitions.
|
||||||
|
|
||||||
Checks that every btc-like coin is properly filled out, reports duplicate symbols,
|
Checks that every btc-like coin is properly filled out, reports duplicate symbols,
|
||||||
@ -703,19 +719,19 @@ def check(backend, icons, show_duplicates):
|
|||||||
@click.option("-d", "--device", metavar="NAME", help="Only include coins supported on a given device")
|
@click.option("-d", "--device", metavar="NAME", help="Only include coins supported on a given device")
|
||||||
# fmt: on
|
# fmt: on
|
||||||
def dump(
|
def dump(
|
||||||
outfile,
|
outfile: TextIO,
|
||||||
support,
|
support: bool,
|
||||||
pretty,
|
pretty: bool,
|
||||||
flat_list,
|
flat_list: bool,
|
||||||
include,
|
include: tuple[str, ...],
|
||||||
exclude,
|
exclude: tuple[str, ...],
|
||||||
include_type,
|
include_type: tuple[str, ...],
|
||||||
exclude_type,
|
exclude_type: tuple[str, ...],
|
||||||
filter,
|
filter: tuple[str, ...],
|
||||||
filter_exclude,
|
filter_exclude: tuple[str, ...],
|
||||||
exclude_tokens,
|
exclude_tokens: bool,
|
||||||
device,
|
device: str,
|
||||||
):
|
) -> None:
|
||||||
"""Dump coin data in JSON format.
|
"""Dump coin data in JSON format.
|
||||||
|
|
||||||
This file is structured the same as the internal data. That is, top-level object
|
This file is structured the same as the internal data. That is, top-level object
|
||||||
@ -744,7 +760,7 @@ def dump(
|
|||||||
so '-f name=bit*' finds all coins whose names start with "bit" or "Bit".
|
so '-f name=bit*' finds all coins whose names start with "bit" or "Bit".
|
||||||
"""
|
"""
|
||||||
if exclude_tokens:
|
if exclude_tokens:
|
||||||
exclude_type = ("erc20",)
|
exclude_type = ["erc20"]
|
||||||
|
|
||||||
if include and exclude:
|
if include and exclude:
|
||||||
raise click.ClickException(
|
raise click.ClickException(
|
||||||
@ -776,7 +792,7 @@ def dump(
|
|||||||
# always exclude 'address_bytes', not encodable in JSON
|
# always exclude 'address_bytes', not encodable in JSON
|
||||||
exclude += ("address_bytes",)
|
exclude += ("address_bytes",)
|
||||||
|
|
||||||
def should_include_coin(coin):
|
def should_include_coin(coin: Coin) -> bool:
|
||||||
for field, filter in include_filters:
|
for field, filter in include_filters:
|
||||||
filter = filter.lower()
|
filter = filter.lower()
|
||||||
if field not in coin:
|
if field not in coin:
|
||||||
@ -795,11 +811,11 @@ def dump(
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def modify_coin(coin):
|
def modify_coin(coin: Coin) -> Coin:
|
||||||
if include:
|
if include:
|
||||||
return {k: v for k, v in coin.items() if k in include}
|
return cast(Coin, {k: v for k, v in coin.items() if k in include})
|
||||||
else:
|
else:
|
||||||
return {k: v for k, v in coin.items() if k not in exclude}
|
return cast(Coin, {k: v for k, v in coin.items() if k not in exclude})
|
||||||
|
|
||||||
for key, coinlist in coins_dict.items():
|
for key, coinlist in coins_dict.items():
|
||||||
coins_dict[key] = [modify_coin(c) for c in coinlist if should_include_coin(c)]
|
coins_dict[key] = [modify_coin(c) for c in coinlist if should_include_coin(c)]
|
||||||
@ -822,7 +838,9 @@ def dump(
|
|||||||
@click.option("-v", "--verbose", is_flag=True, help="Print rendered file names")
|
@click.option("-v", "--verbose", is_flag=True, help="Print rendered file names")
|
||||||
@click.option("-b", "--bitcoin-only", is_flag=True, help="Accept only Bitcoin coins")
|
@click.option("-b", "--bitcoin-only", is_flag=True, help="Accept only Bitcoin coins")
|
||||||
# fmt: on
|
# fmt: on
|
||||||
def render(paths, outfile, verbose, bitcoin_only):
|
def render(
|
||||||
|
paths: tuple[str, ...], outfile: TextIO, verbose: bool, bitcoin_only: bool
|
||||||
|
) -> None:
|
||||||
"""Generate source code from Mako templates.
|
"""Generate source code from Mako templates.
|
||||||
|
|
||||||
For every "foo.bar.mako" filename passed, runs the template and
|
For every "foo.bar.mako" filename passed, runs the template and
|
||||||
@ -857,9 +875,9 @@ def render(paths, outfile, verbose, bitcoin_only):
|
|||||||
for key, value in support_info.items():
|
for key, value in support_info.items():
|
||||||
support_info[key] = Munch(value)
|
support_info[key] = Munch(value)
|
||||||
|
|
||||||
def do_render(src, dst):
|
def do_render(src: str, dst: TextIO) -> None:
|
||||||
if verbose:
|
if verbose:
|
||||||
click.echo(f"Rendering {src} => {dst}")
|
click.echo(f"Rendering {src} => {dst.name}")
|
||||||
render_file(src, dst, defs, support_info)
|
render_file(src, dst, defs, support_info)
|
||||||
|
|
||||||
# single in-out case
|
# single in-out case
|
||||||
@ -869,9 +887,9 @@ def render(paths, outfile, verbose, bitcoin_only):
|
|||||||
|
|
||||||
# find files in directories
|
# find files in directories
|
||||||
if not paths:
|
if not paths:
|
||||||
paths = ["."]
|
paths = (".",)
|
||||||
|
|
||||||
files = []
|
files: list[str] = []
|
||||||
for path in paths:
|
for path in paths:
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
click.echo(f"Path {path} does not exist")
|
click.echo(f"Path {path} does not exist")
|
||||||
|
Loading…
Reference in New Issue
Block a user