diff --git a/common/tools/coin_info.py b/common/tools/coin_info.py index fd2e383aff..e2f37e9af9 100755 --- a/common/tools/coin_info.py +++ b/common/tools/coin_info.py @@ -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() diff --git a/common/tools/cointool.py b/common/tools/cointool.py index 20120ba7e8..422ee5cde5 100755 --- a/common/tools/cointool.py +++ b/common/tools/cointool.py @@ -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")