1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-11-21 23:18:13 +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:
grdddj 2022-05-23 16:19:19 +02:00 committed by Jiří Musil
parent 5671bd037b
commit 519f79f9eb
2 changed files with 257 additions and 107 deletions

View File

@ -1,10 +1,13 @@
#!/usr/bin/env python3
from __future__ import annotations
import json
import logging
import os
import re
from collections import OrderedDict, defaultdict
from pathlib import Path
from typing import Any, Callable, Iterable, Iterator, Literal, TypedDict, cast
try:
import requests
@ -21,7 +24,125 @@ else:
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."""
if len(path) == 1 and isinstance(path[0], Path):
file = path[0]
@ -34,7 +155,7 @@ def load_json(*path):
# ====== CoinsInfo ======
class CoinsInfo(dict):
class CoinsInfo(dict[str, Coins]):
"""Collection of information about all known kinds of coins.
It contains the following lists:
@ -47,13 +168,13 @@ class CoinsInfo(dict):
Accessible as a dict or by attribute: `info["misc"] == info.misc`
"""
def as_list(self):
def as_list(self) -> Coins:
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()}
def __getattr__(self, attr):
def __getattr__(self, attr: str) -> Coins:
if attr in self:
return self[attr]
else:
@ -63,7 +184,14 @@ class CoinsInfo(dict):
# ====== 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
if val is None:
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 types is not str:
raise TypeError("Wrong type for regex check")
assert isinstance(val, str)
if not re.search(regex, val):
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}")
def check_key(key, types, optional=False, **kwargs):
def do_check(coin):
def check_key(
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 optional:
return
@ -152,15 +283,15 @@ BTC_CHECKS = [
]
def validate_btc(coin):
errors = []
def validate_btc(coin: Coin) -> list[str]:
errors: list[str] = []
for check in BTC_CHECKS:
try:
check(coin)
except Exception as e:
errors.append(str(e))
magics = [
magics: list[int] = [
coin[k]
for k in (
"xprv_magic",
@ -208,11 +339,11 @@ def validate_btc(coin):
# ======= Coin json loaders =======
def _load_btc_coins():
def _load_btc_coins() -> Coins:
"""Load btc-like coins from `bitcoin/*.json`"""
coins = []
coins: Coins = []
for file in DEFS_DIR.glob("bitcoin/*.json"):
coin = load_json(file)
coin: Coin = load_json(file)
coin.update(
name=coin["coin_label"],
shortcut=coin["coin_shortcut"],
@ -224,10 +355,10 @@ def _load_btc_coins():
return coins
def _load_ethereum_networks():
def _load_ethereum_networks() -> Coins:
"""Load ethereum networks from `ethereum/networks.json`"""
chains_path = DEFS_DIR / "ethereum" / "chains" / "_data" / "chains"
networks = []
networks: Coins = []
for chain in sorted(
chains_path.glob("eip155-*.json"),
key=lambda x: int(x.stem.replace("eip155-", "")),
@ -261,21 +392,21 @@ def _load_ethereum_networks():
url=chain_data["infoURL"],
key=f"eth:{shortcut}",
)
networks.append(network)
networks.append(cast(Coin, network))
return networks
def _load_erc20_tokens():
def _load_erc20_tokens() -> Coins:
"""Load ERC20 tokens from `ethereum/tokens` submodule."""
networks = _load_ethereum_networks()
tokens = []
tokens: Coins = []
for network in networks:
chain = network["chain"]
chain_path = DEFS_DIR / "ethereum" / "tokens" / "tokens" / chain
for file in sorted(chain_path.glob("*.json")):
token = load_json(file)
token: Coin = load_json(file)
token.update(
chain=chain,
chain_id=network["chain_id"],
@ -288,26 +419,26 @@ def _load_erc20_tokens():
return tokens
def _load_nem_mosaics():
def _load_nem_mosaics() -> Coins:
"""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:
shortcut = mosaic["ticker"].strip()
mosaic.update(shortcut=shortcut, key=f"nem:{shortcut}")
return mosaics
def _load_misc():
def _load_misc() -> Coins:
"""Loads miscellaneous networks from `misc/misc.json`"""
others = load_json("misc/misc.json")
others: Coins = load_json("misc/misc.json")
for other in others:
other.update(key=f"misc:{other['shortcut']}")
return others
def _load_fido_apps():
def _load_fido_apps() -> FidoApps:
"""Load FIDO apps from `fido/*.json`"""
apps = []
apps: FidoApps = []
for file in sorted(DEFS_DIR.glob("fido/*.json")):
app_name = file.stem.lower()
app = load_json(file)
@ -335,28 +466,28 @@ MISSING_SUPPORT_MEANS_NO = ("connect", "suite")
VERSIONED_SUPPORT_INFO = ("trezor1", "trezor2")
def get_support_data():
def get_support_data() -> SupportData:
"""Get raw support data from `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"""
if not requests:
raise RuntimeError("requests library is required for getting release info")
latest = {}
latest: dict[str, Any] = {}
for v in ("1", "2"):
releases = requests.get(RELEASES_URL.format(v)).json()
latest["trezor" + v] = max(tuple(r["version"]) for r in releases)
return latest
def is_token(coin):
def is_token(coin: Coin) -> bool:
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.
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)
* if the coin doesn't have an entry, its support status is `None`
"""
support_info = {}
support_info_item = {}
key = coin["key"]
for device, values in support_data.items():
assert isinstance(values, dict)
if key in values["unsupported"]:
support_value = False
support_value: Any = False
elif key in values["supported"]:
support_value = values["supported"][key]
elif device in MISSING_SUPPORT_MEANS_NO:
support_value = False
else:
support_value = None
support_info[device] = support_value
return support_info
support_info_item[device] = support_value
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.
Takes a collection of coins and generates a support-info entry for each.
@ -401,7 +533,7 @@ def support_info(coins):
coins = coins.values()
support_data = get_support_data()
support = {}
support: SupportInfo = {}
for coin in coins:
support[coin["key"]] = support_info_single(support_data, coin)
@ -411,19 +543,19 @@ def support_info(coins):
# ====== 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"""
for coin in coins:
if not all(coin.get(k) for k in ("name", "shortcut", "key")):
raise ValueError(coin)
def symbol_from_shortcut(shortcut):
def symbol_from_shortcut(shortcut: str) -> tuple[str, str]:
symsplit = shortcut.split(" ", maxsplit=1)
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.
"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
it's explicitly marked as `false` in `duplicity_overrides.json`.
"""
dup_symbols = defaultdict(list)
dup_symbols: CoinBuckets = defaultdict(list)
for coin in coins:
symbol, _ = symbol_from_shortcut(coin["shortcut"].lower())
@ -451,9 +583,9 @@ def mark_duplicate_shortcuts(coins):
return dup_symbols
def apply_duplicity_overrides(coins):
def apply_duplicity_overrides(coins: Coins) -> Coins:
overrides = load_json("duplicity_overrides.json")
override_bucket = []
override_bucket: Coins = []
for coin in coins:
override_value = overrides.get(coin["key"])
if override_value is True:
@ -464,7 +596,7 @@ def apply_duplicity_overrides(coins):
return override_bucket
def deduplicate_erc20(buckets, networks):
def deduplicate_erc20(buckets: CoinBuckets, networks: Coins) -> None:
"""Apply further processing to ERC20 duplicate buckets.
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"]}
def clear_bucket(bucket):
def clear_bucket(bucket: Coins) -> None:
# allow all coins, except those that are explicitly marked through overrides
for coin in bucket:
coin["duplicate"] = False
@ -531,8 +663,8 @@ def deduplicate_erc20(buckets, networks):
clear_bucket(bucket)
def deduplicate_keys(all_coins):
dups = defaultdict(list)
def deduplicate_keys(all_coins: Coins) -> None:
dups: CoinBuckets = defaultdict(list)
for coin in all_coins:
dups[coin["key"]].append(coin)
@ -549,7 +681,7 @@ def deduplicate_keys(all_coins):
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")
for coins in all_coins.values():
for coin in coins:
@ -561,14 +693,14 @@ def fill_blockchain_links(all_coins):
coin["blockbook"] = []
def _btc_sort_key(coin):
def _btc_sort_key(coin: Coin) -> str:
if coin["name"] in ("Bitcoin", "Testnet", "Regtest"):
return "000000" + coin["name"]
else:
return coin["name"]
def collect_coin_info():
def collect_coin_info() -> CoinsInfo:
"""Returns all definition as dict organized by coin type.
`coins` for btc-like coins,
`eth` for ethereum networks,
@ -592,7 +724,7 @@ def collect_coin_info():
return all_coins
def sort_coin_infos(all_coins):
def sort_coin_infos(all_coins: CoinsInfo) -> None:
for k, coins in all_coins.items():
if k == "bitcoin":
coins.sort(key=_btc_sort_key)
@ -606,7 +738,7 @@ def sort_coin_infos(all_coins):
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.
Returns the CoinsInfo object and duplicate buckets.
@ -626,7 +758,7 @@ def coin_info_with_duplicates():
return all_coins, buckets
def coin_info():
def coin_info() -> CoinsInfo:
"""Collects coin info, fills out support info and returns the result.
Does not auto-delete duplicates. This should now be based on support info.
@ -638,12 +770,12 @@ def coin_info():
return all_coins
def fido_info():
def fido_info() -> FidoApps:
"""Returns info about known FIDO/U2F apps."""
return _load_fido_apps()
def search(coins, keyword):
def search(coins: CoinsInfo | Coins, keyword: str) -> Iterator[Any]:
kwl = keyword.lower()
if isinstance(coins, CoinsInfo):
coins = coins.as_list()

View File

@ -1,4 +1,6 @@
#!/usr/bin/env python3
from __future__ import annotations
import fnmatch
import glob
import json
@ -8,10 +10,12 @@ import re
import sys
from collections import defaultdict
from hashlib import sha256
from typing import Any, Callable, Iterator, TextIO, cast
import click
import coin_info
from coin_info import Coin, CoinBuckets, Coins, CoinsInfo, FidoApps, SupportInfo
try:
import termcolor
@ -44,7 +48,9 @@ except ImportError:
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:
return string
else:
@ -57,7 +63,7 @@ def crayon(color, string, bold=False, dim=False):
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)
if level == logging.DEBUG:
prefix = crayon("blue", prefix, bold=False)
@ -73,11 +79,11 @@ def print_log(level, *args, **kwargs):
# ======= Mako management ======
def c_str_filter(b):
def c_str_filter(b: Any) -> str:
if b is None:
return "NULL"
def hexescape(c):
def hexescape(c: bytes) -> str:
return rf"\x{c:02x}"
if isinstance(b, bytes):
@ -86,7 +92,7 @@ def c_str_filter(b):
return json.dumps(b)
def black_repr_filter(val):
def black_repr_filter(val: Any) -> str:
if isinstance(val, str):
if '"' in val:
return repr(val)
@ -98,12 +104,14 @@ def black_repr_filter(val):
return repr(val)
def ascii_filter(s):
def ascii_filter(s: str) -> str:
return re.sub("[^ -\x7e]", "_", s)
def make_support_filter(support_info):
def supported_on(device, coins):
def make_support_filter(
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 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`.
`src` is a filename, `dst` is an open file object.
@ -134,14 +144,14 @@ def render_file(src, dst, coins, support_info):
# ====== validation functions ======
def mark_unsupported(support_info, coins):
def mark_unsupported(support_info: SupportInfo, coins: Coins) -> None:
for coin in coins:
key = coin["key"]
# checking for explicit False because None means unknown
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."""
keylist = coin["key"].split(":")
if keylist[-1].isdigit():
@ -153,9 +163,9 @@ def highlight_key(coin, color):
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."""
collisions = defaultdict(list)
collisions: CoinBuckets = defaultdict(list)
for coin in coins:
values = coin[field]
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}
def check_eth(coins):
def check_eth(coins: Coins) -> bool:
check_passed = True
chains = find_collisions(coins, "chain")
for key, bucket in chains.items():
@ -176,7 +186,7 @@ def check_eth(coins):
return check_passed
def check_btc(coins):
def check_btc(coins: Coins) -> bool:
check_passed = True
# validate individual coin data
@ -187,9 +197,9 @@ def check_btc(coins):
print_log(logging.ERROR, "invalid definition for", coin["name"])
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."""
coin_strings = []
coin_strings: list[str] = []
for coin in bucket:
name = coin["name"]
prefix = ""
@ -206,7 +216,12 @@ def check_btc(coins):
coin_strings.append(prefix + hl)
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.
For each bucket, if there are any collision with a mainnet, print it.
@ -268,7 +283,7 @@ def check_btc(coins):
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`.
`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.
"""
def coin_str(coin):
def coin_str(coin: Coin) -> str:
"""Colorize coins. Tokens are cyan, nontokens are red. Coins that are NOT
marked duplicate get a green asterisk.
"""
@ -347,7 +362,7 @@ def check_dups(buckets, print_at_level=logging.WARNING):
return check_passed
def check_backends(coins):
def check_backends(coins: Coins) -> bool:
check_passed = True
for coin in coins:
genesis_block = coin.get("hash_genesis_block")
@ -357,6 +372,7 @@ def check_backends(coins):
for backend in backends:
print("checking", backend, "... ", end="", flush=True)
try:
assert requests is not None
j = requests.get(backend + "/api/block-index/0").json()
if j["blockHash"] != genesis_block:
raise RuntimeError("genesis block mismatch")
@ -368,7 +384,7 @@ def check_backends(coins):
return check_passed
def check_icons(coins):
def check_icons(coins: Coins) -> bool:
check_passed = True
for coin in coins:
key = coin["key"]
@ -394,8 +410,8 @@ def check_icons(coins):
IGNORE_NONUNIFORM_KEYS = frozenset(("unsupported", "duplicate"))
def check_key_uniformity(coins):
keysets = defaultdict(list)
def check_key_uniformity(coins: Coins) -> bool:
keysets: dict[frozenset[str], Coins] = defaultdict(list)
for coin in coins:
keyset = frozenset(coin.keys()) | IGNORE_NONUNIFORM_KEYS
keysets[keyset].append(coin)
@ -426,7 +442,7 @@ def check_key_uniformity(coins):
return False
def check_segwit(coins):
def check_segwit(coins: Coins) -> bool:
for coin in coins:
segwit = coin["segwit"]
segwit_fields = [
@ -471,7 +487,7 @@ FIDO_KNOWN_KEYS = frozenset(
)
def check_fido(apps):
def check_fido(apps: FidoApps) -> bool:
check_passed = True
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)
check_passed = False
domain_hashes = {}
domain_hashes: dict[bytes, str] = {}
for app in apps:
if "webauthn" in app:
for domain in app["webauthn"]:
@ -574,7 +590,7 @@ def check_fido(apps):
default=sys.stdout.isatty(),
help="Force colored output on/off",
)
def cli(colors):
def cli(colors: bool) -> None:
global USE_COLORS
USE_COLORS = colors
@ -585,7 +601,7 @@ def cli(colors):
@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.")
# fmt: on
def check(backend, icons, show_duplicates):
def check(backend: bool, icons: bool, show_duplicates: str) -> None:
"""Validate coin definitions.
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")
# fmt: on
def dump(
outfile,
support,
pretty,
flat_list,
include,
exclude,
include_type,
exclude_type,
filter,
filter_exclude,
exclude_tokens,
device,
):
outfile: TextIO,
support: bool,
pretty: bool,
flat_list: bool,
include: tuple[str, ...],
exclude: tuple[str, ...],
include_type: tuple[str, ...],
exclude_type: tuple[str, ...],
filter: tuple[str, ...],
filter_exclude: tuple[str, ...],
exclude_tokens: bool,
device: str,
) -> None:
"""Dump coin data in JSON format.
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".
"""
if exclude_tokens:
exclude_type = ("erc20",)
exclude_type = ["erc20"]
if include and exclude:
raise click.ClickException(
@ -776,7 +792,7 @@ def dump(
# always exclude 'address_bytes', not encodable in JSON
exclude += ("address_bytes",)
def should_include_coin(coin):
def should_include_coin(coin: Coin) -> bool:
for field, filter in include_filters:
filter = filter.lower()
if field not in coin:
@ -795,11 +811,11 @@ def dump(
return False
return True
def modify_coin(coin):
def modify_coin(coin: Coin) -> Coin:
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:
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():
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("-b", "--bitcoin-only", is_flag=True, help="Accept only Bitcoin coins")
# 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.
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():
support_info[key] = Munch(value)
def do_render(src, dst):
def do_render(src: str, dst: TextIO) -> None:
if verbose:
click.echo(f"Rendering {src} => {dst}")
click.echo(f"Rendering {src} => {dst.name}")
render_file(src, dst, defs, support_info)
# single in-out case
@ -869,9 +887,9 @@ def render(paths, outfile, verbose, bitcoin_only):
# find files in directories
if not paths:
paths = ["."]
paths = (".",)
files = []
files: list[str] = []
for path in paths:
if not os.path.exists(path):
click.echo(f"Path {path} does not exist")