diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml new file mode 100644 index 000000000..588b04a1d --- /dev/null +++ b/.github/workflows/mypy.yml @@ -0,0 +1,37 @@ +name: Type checking with mypy + +on: + push: + branches: + - '**' + pull_request: + branches: + - main + +jobs: + mypy: + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + matrix: + python-version: [3.8, "3.10", "3.11"] + + steps: + - name: Checkout Code + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Setup Python environment + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + pip install --upgrade pip + pip install mypy>=1.0 + + - name: Run mypy + run: | + mypy pycoin diff --git a/.github/workflows/test-py27.yml b/.github/workflows/test-py27.yml deleted file mode 100644 index 5423549fb..000000000 --- a/.github/workflows/test-py27.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Tests and coverage - -on: - push: - branches: - - '**' - tags: - - '**' - -jobs: - build: - runs-on: ubuntu-latest - timeout-minutes: 30 - strategy: - fail-fast: false - max-parallel: 4 - matrix: - python-version: [2.7] - - steps: - - name: Checkout Code - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: Set up Python 2.7 - run: | - sudo apt-get update - sudo apt-get install -y software-properties-common - sudo add-apt-repository universe - sudo apt-get install -y python2 - python2 --version - - - name: Install pip for Python 2.7 - run: | - curl https://bootstrap.pypa.io/pip/2.7/get-pip.py -o get-pip.py - sudo python2 get-pip.py - python2 -m pip --version - - - name: Test core code with pytest - run: | - pip install coverage pytest - coverage run -m pytest tests diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c5ff64295..048090fd6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,7 +15,7 @@ jobs: fail-fast: false max-parallel: 4 matrix: - python-version: [3.7, 3.8, 3.9, "3.10", "3.11", "3.12", "3.13"] + python-version: [3.8, 3.9, "3.10", "3.11", "3.12", "3.13"] steps: - name: Checkout Code diff --git a/.gitignore b/.gitignore index 18df683cd..a882f2ad5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,8 @@ files.txt *egg-info .DS_STORE .tox +.coverage +.coverage.* +htmlcov/ +.pytest_cache/ +.mypy_cache/ diff --git a/MANIFEST.in b/MANIFEST.in index b3db1eb18..b8764253d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,2 @@ include CHANGES CREDITS LICENSE *.md *.txt +include pycoin/py.typed diff --git a/README.md b/README.md index 510c90a2b..46bc9a5d9 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ pycoin -- Python Cryptocoin Utilities ===================================== The pycoin library implements many utilities useful when dealing with bitcoin and some bitcoin-like -alt-coins. It has been tested with Python 2.7, 3.7-3.13. +alt-coins. It requires Python 3.8 or higher. See also [pycoinnet](http://github.com/richardkiss/pycoinnet/) for a library that speaks the bitcoin protocol. diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 000000000..7cef3f5a1 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,12 @@ +[mypy] +python_version = 3.8 +warn_return_any = False +warn_unused_configs = True +disallow_untyped_defs = False +ignore_missing_imports = True +strict_optional = False +files = pycoin + +# Specific module ignores can be added as needed +# [mypy-pycoin.contrib.*] +# ignore_errors = True diff --git a/pycoin/bloomfilter.py b/pycoin/bloomfilter.py index 335c56847..4017f2919 100644 --- a/pycoin/bloomfilter.py +++ b/pycoin/bloomfilter.py @@ -1,5 +1,6 @@ import math import struct +from typing import Tuple from pycoin.encoding.b58 import a2b_hashed_base58 from pycoin.intbytes import indexbytes @@ -7,7 +8,7 @@ LOG_2 = math.log(2) -def filter_size_required(element_count, false_positive_probability): +def filter_size_required(element_count: int, false_positive_probability: float) -> int: # The size S of the filter in bytes is given by # (-1 / pow(log(2), 2) * N * log(P)) / 8 # Of course you must ensure it does not go over the maximum size @@ -17,7 +18,7 @@ def filter_size_required(element_count, false_positive_probability): return min(36000, int(((-1 / pow(LOG_2, 2) * element_count * lfpp)+7) // 8)) -def hash_function_count_required(filter_size, element_count): +def hash_function_count_required(filter_size: int, element_count: int) -> int: # The number of hash functions required is given by S * 8 / N * log(2). return int(filter_size * 8.0 / element_count * LOG_2 + 0.5) @@ -25,7 +26,7 @@ def hash_function_count_required(filter_size, element_count): class BloomFilter(object): MASK_ARRAY = [1 << _ for _ in range(8)] - def __init__(self, size_in_bytes, hash_function_count, tweak): + def __init__(self, size_in_bytes: int, hash_function_count: int, tweak: int) -> None: if size_in_bytes > 36000: raise ValueError("too large") self.filter_bytes = bytearray(size_in_bytes) @@ -33,7 +34,7 @@ def __init__(self, size_in_bytes, hash_function_count, tweak): self.hash_function_count = hash_function_count self.tweak = tweak - def add_item(self, item_bytes): + def add_item(self, item_bytes: bytes) -> None: for hash_index in range(self.hash_function_count): seed = hash_index * 0xFBA4C795 + self.tweak self.set_bit(murmur3(item_bytes, seed=seed) % self.bit_count) @@ -63,13 +64,13 @@ def check_bit(self, v): byte_index, mask = self._index_for_bit(v) return (self.filter_bytes[byte_index] & mask) == mask - def filter_load_params(self): + def filter_load_params(self) -> Tuple[bytearray, int, int]: return self.filter_bytes, self.hash_function_count, self.tweak # http://stackoverflow.com/questions/13305290/is-there-a-pure-python-implementation-of-murmurhash -def murmur3(data, seed=0): +def murmur3(data: bytes, seed: int = 0) -> int: c1 = 0xcc9e2d51 c2 = 0x1b873593 diff --git a/pycoin/cmds/b58.py b/pycoin/cmds/b58.py index 67ffe769a..e4824366b 100755 --- a/pycoin/cmds/b58.py +++ b/pycoin/cmds/b58.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -from __future__ import print_function - import argparse from pycoin.encoding.b58 import a2b_base58, b2a_base58, a2b_hashed_base58, b2a_hashed_base58 diff --git a/pycoin/cmds/dump.py b/pycoin/cmds/dump.py index 9f4d3db67..c519e86b7 100644 --- a/pycoin/cmds/dump.py +++ b/pycoin/cmds/dump.py @@ -1,4 +1,3 @@ -from __future__ import print_function import datetime diff --git a/pycoin/cmds/keychain.py b/pycoin/cmds/keychain.py index c5a059988..4f03f0855 100755 --- a/pycoin/cmds/keychain.py +++ b/pycoin/cmds/keychain.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -from __future__ import print_function - import argparse import sqlite3 import sys diff --git a/pycoin/cmds/ku.py b/pycoin/cmds/ku.py index 799f5afb8..f6ae763c4 100755 --- a/pycoin/cmds/ku.py +++ b/pycoin/cmds/ku.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -from __future__ import print_function - import argparse import json import re diff --git a/pycoin/cmds/msg.py b/pycoin/cmds/msg.py index 5796f9dba..f57287266 100755 --- a/pycoin/cmds/msg.py +++ b/pycoin/cmds/msg.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -from __future__ import print_function - import argparse import sys diff --git a/pycoin/cmds/tx.py b/pycoin/cmds/tx.py index 794166873..8026f242e 100755 --- a/pycoin/cmds/tx.py +++ b/pycoin/cmds/tx.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -from __future__ import print_function - import argparse import calendar import codecs diff --git a/pycoin/coins/SolutionChecker.py b/pycoin/coins/SolutionChecker.py index 1932255de..a77e6554a 100644 --- a/pycoin/coins/SolutionChecker.py +++ b/pycoin/coins/SolutionChecker.py @@ -18,4 +18,4 @@ def check_solution(self, tx_context, traceback_f=None, *args, **kwargs): tx_context: information about the transaction that the VM may need traceback_f: a function invoked on occasion to check intermediate state """ - raise NotImplemented() + raise NotImplementedError() diff --git a/pycoin/coins/Tx.py b/pycoin/coins/Tx.py index d6336e92a..8800d64c1 100644 --- a/pycoin/coins/Tx.py +++ b/pycoin/coins/Tx.py @@ -17,7 +17,7 @@ class Tx(object): @classmethod def parse(class_, f): """Parse a transaction Tx from the file-like object f.""" - raise NotImplemented() + raise NotImplementedError() @classmethod def from_bin(class_, blob): @@ -53,11 +53,11 @@ def from_hex(class_, hex_string): return class_.from_bin(h2b(hex_string)) def __init__(self, *args, **kwargs): - raise NotImplemented() + raise NotImplementedError() def stream(self, f, *args, **kwargs): """Stream a transaction Tx to the file-like object f.""" - raise NotImplemented() + raise NotImplementedError() def as_bin(self, *args, **kwargs): """Returns a binary blob containing the streamed transaction. @@ -81,7 +81,7 @@ def as_hex(self, *args, **kwargs): def hash(self, hash_type=None): """Return the hash for this Tx object.""" - raise NotImplemented() + raise NotImplementedError() def id(self): """Return the human-readable hash for this Tx object.""" @@ -97,16 +97,16 @@ def tx_outs_as_spendable(self, block_index_available=0): for tx_out_index, tx_out in enumerate(self.txs_out)] def __str__(self): - raise NotImplemented() + raise NotImplementedError() def __repr__(self): - raise NotImplemented() + raise NotImplementedError() def check(self): """ Basic checks that don't depend on network or block context. """ - raise NotImplemented() + raise NotImplementedError() """ The functions below here deal with an optional additional parameter: "unspents". diff --git a/pycoin/coins/TxIn.py b/pycoin/coins/TxIn.py index d2415548d..16f6a2212 100644 --- a/pycoin/coins/TxIn.py +++ b/pycoin/coins/TxIn.py @@ -5,16 +5,16 @@ class TxIn(object): @classmethod def parse(class_, f): - raise NotImplemented() + raise NotImplementedError() def __init__(self, *args, **kwargs): - raise NotImplemented() + raise NotImplementedError() def stream(self, f): - raise NotImplemented() + raise NotImplementedError() def __str__(self): - raise NotImplemented() + raise NotImplementedError() """ diff --git a/pycoin/coins/TxOut.py b/pycoin/coins/TxOut.py index d35aefff0..5080b2fad 100644 --- a/pycoin/coins/TxOut.py +++ b/pycoin/coins/TxOut.py @@ -5,16 +5,16 @@ class TxOut(object): @classmethod def parse(class_, f): - raise NotImplemented() + raise NotImplementedError() def __init__(self, *args, **kwargs): - raise NotImplemented() + raise NotImplementedError() def stream(self, f): - raise NotImplemented() + raise NotImplementedError() def __str__(self): - raise NotImplemented() + raise NotImplementedError() """ diff --git a/pycoin/ecdsa/intstream.py b/pycoin/ecdsa/intstream.py index ecd8a055a..44bd5d5f8 100644 --- a/pycoin/ecdsa/intstream.py +++ b/pycoin/ecdsa/intstream.py @@ -1,40 +1,11 @@ +from typing import Literal -from pycoin.intbytes import iterbytes, byte2int - -def _to_bytes(v, length, byteorder="big"): +def to_bytes(v: int, length: int, byteorder: Literal["big", "little"] = "big") -> bytes: """This is the same functionality as ``int.to_bytes`` in python 3""" return v.to_bytes(length, byteorder=byteorder) -def _from_bytes(bytes, byteorder="big", signed=False): +def from_bytes(data: bytes, byteorder: Literal["big", "little"] = "big", signed: bool = False) -> int: """This is the same functionality as ``int.from_bytes`` in python 3""" - return int.from_bytes(bytes, byteorder=byteorder, signed=signed) - - -if hasattr(int, "to_bytes"): - to_bytes = _to_bytes - from_bytes = _from_bytes -else: - def to_bytes(v, length, byteorder="big"): - """This is the same functionality as ``int.to_bytes`` in python 3""" - ba = bytearray() - for i in range(length): - mod = v & 0xff - v >>= 8 - ba.append(mod) - if byteorder == "big": - ba.reverse() - return bytes(ba) - - def from_bytes(bytes, byteorder="big", signed=False): - """This is the same functionality as ``int.from_bytes`` in python 3""" - if byteorder != "big": - bytes = reversed(bytes) - v = 0 - for c in iterbytes(bytes): - v <<= 8 - v += c - if signed and byte2int(bytes) & 0x80: - v = v - (1 << (8*len(bytes))) - return v + return int.from_bytes(data, byteorder=byteorder, signed=signed) diff --git a/pycoin/ecdsa/rfc6979.py b/pycoin/ecdsa/rfc6979.py index 1af9274bd..6672d115c 100644 --- a/pycoin/ecdsa/rfc6979.py +++ b/pycoin/ecdsa/rfc6979.py @@ -1,24 +1,16 @@ import hashlib import hmac +from typing import Callable, Any from . import intstream -if hasattr(1, "bit_length"): - def bit_length(v): - "the ``int.bit_length`` in `python 3 `_" - return v.bit_length() -else: - def bit_length(self): - "the ``int.bit_length`` in `python 3 `_" - # compared to "while n>0: bl +=1 ; n >>= 1", this is much faster in both python2 and pypy - # code taken from the link above - s = bin(self) # binary representation: bin(-37) --> '-0b100101' - s = s.lstrip('-0b') # remove leading zeros and minus sign - return len(s) # len('100101') --> 6 +def bit_length(v: int) -> int: + "the ``int.bit_length`` in `python 3 `_" + return v.bit_length() -def deterministic_generate_k(generator_order, secret_exponent, val, hash_f=hashlib.sha256): +def deterministic_generate_k(generator_order: int, secret_exponent: int, val: int, hash_f: Callable[..., Any] = hashlib.sha256) -> int: """ :param generator_order: result from `pycoin.ecdsa.Generator.Generator.order`, necessary to ensure the k value is within bound diff --git a/pycoin/encoding/bytes32.py b/pycoin/encoding/bytes32.py index a757c558b..7ca0aa165 100644 --- a/pycoin/encoding/bytes32.py +++ b/pycoin/encoding/bytes32.py @@ -1,24 +1,10 @@ -if hasattr(int, "to_bytes"): - def to_bytes_32(v): - return v.to_bytes(32, byteorder="big") - - def from_bytes_32(v): - return int.from_bytes(v, byteorder="big") -else: - from .base_conversion import from_long, to_long - from ..intbytes import byte2int - - def to_bytes_32(v): - v = from_long(v, 0, 256, lambda x: x) - if len(v) > 32: - raise ValueError("input to to_bytes_32 is too large") - return ((b'\0' * 32) + v)[-32:] - - def from_bytes_32(v): - if len(v) > 32: - raise OverflowError("int too big to convert") - return to_long(256, byte2int, v)[0] +def to_bytes_32(v: int) -> bytes: + return v.to_bytes(32, byteorder="big") + + +def from_bytes_32(v: bytes) -> int: + return int.from_bytes(v, byteorder="big") """ diff --git a/pycoin/encoding/hexbytes.py b/pycoin/encoding/hexbytes.py index 326373735..973c67fe6 100644 --- a/pycoin/encoding/hexbytes.py +++ b/pycoin/encoding/hexbytes.py @@ -1,7 +1,7 @@ import binascii -def h2b(h): +def h2b(h: str) -> bytes: """ A version of binascii.unhexlify that accepts unicode. This is no longer necessary as of Python 3.3. But it doesn't hurt. @@ -15,15 +15,15 @@ def h2b(h): raise ValueError("h2b failed on %s" % h) -def h2b_rev(h): +def h2b_rev(h: str) -> bytes: return h2b(h)[::-1] -def b2h(the_bytes): +def b2h(the_bytes: bytes) -> str: return binascii.hexlify(the_bytes).decode("utf8") -def b2h_rev(the_bytes): +def b2h_rev(the_bytes: bytes) -> str: return b2h(bytearray(reversed(the_bytes))) diff --git a/pycoin/intbytes.py b/pycoin/intbytes.py index e4b177fe6..f3f7e007f 100644 --- a/pycoin/intbytes.py +++ b/pycoin/intbytes.py @@ -1,6 +1,5 @@ """ -Provide the following functions, all cribbed from the library -`six `_. +Provide the following functions for byte operations in Python 3: iterbytes(buf): return an iterator of ints corresponding to the bytes of buf @@ -15,22 +14,12 @@ turn bs[0] into an int (0-255) """ -import functools -import itertools +from typing import Iterator, Callable import operator import struct -if bytes == str: - iterbytes = functools.partial(itertools.imap, ord) - - def indexbytes(buf, i): - return ord(buf[i]) - int2byte = chr - - def byte2int(bs): - return ord(bs[0]) -else: - iterbytes = iter - indexbytes = operator.getitem - int2byte = struct.Struct(">B").pack - byte2int = operator.itemgetter(0) +# Python 3 implementations +iterbytes: Callable[[bytes], Iterator[int]] = iter +indexbytes: Callable[[bytes, int], int] = operator.getitem +int2byte: Callable[[int], bytes] = struct.Struct(">B").pack +byte2int: Callable[[bytes], int] = operator.itemgetter(0) diff --git a/pycoin/merkle.py b/pycoin/merkle.py index 3574b2bd1..eb541171d 100644 --- a/pycoin/merkle.py +++ b/pycoin/merkle.py @@ -1,15 +1,17 @@ +from typing import List, Callable + from .encoding.hash import double_sha256 from .encoding.hexbytes import h2b_rev -def merkle(hashes, hash_f=double_sha256): +def merkle(hashes: List[bytes], hash_f: Callable[[bytes], bytes] = double_sha256) -> bytes: """Take a list of hashes, and return the root merkle hash.""" while len(hashes) > 1: hashes = merkle_pair(hashes, hash_f) return hashes[0] -def merkle_pair(hashes, hash_f): +def merkle_pair(hashes: List[bytes], hash_f: Callable[[bytes], bytes]) -> List[bytes]: """Take a list of hashes, and return the parent row in the tree of merkle hashes.""" if len(hashes) % 2 == 1: hashes = list(hashes) @@ -20,7 +22,7 @@ def merkle_pair(hashes, hash_f): return items -def test_merkle(): +def test_merkle() -> None: s1 = h2b_rev("56dee62283a06e85e182e2d0b421aceb0eadec3d5f86cdadf9688fc095b72510") assert merkle([s1], double_sha256) == s1 # from block 71043 diff --git a/pycoin/py.typed b/pycoin/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/pycoin/services/agent.py b/pycoin/services/agent.py index 8c87901d7..19a73bdf8 100644 --- a/pycoin/services/agent.py +++ b/pycoin/services/agent.py @@ -1,11 +1,6 @@ from pycoin import version - -try: - import urllib2 as request - from urllib import urlencode # noqa -except ImportError: - from urllib import request - from urllib.parse import urlencode # noqa +from urllib import request +from urllib.parse import urlencode # noqa PYCOIN_AGENT = 'pycoin/%s' % version diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 000000000..a57526d03 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,12 @@ +# Development dependencies for pycoin + +# Testing +pytest>=7.0 +pytest-cov>=3.0 +coverage>=5.0 + +# Type checking +mypy>=1.0 + +# Optional: for testing altcoin support +groestlcoin_hash diff --git a/setup.py b/setup.py index 2205b8f53..34e9db8ad 100755 --- a/setup.py +++ b/setup.py @@ -52,13 +52,16 @@ description="Utilities for Bitcoin and altcoin addresses and transaction manipulation.", long_description=long_description, long_description_content_type='text/markdown', + python_requires='>=3.8', classifiers=[ 'Development Status :: 3 - Alpha', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', 'License :: OSI Approved :: MIT License', 'Topic :: Internet', 'Topic :: Security :: Cryptography', diff --git a/tox.ini b/tox.ini index 381dbc2f5..034ac41f0 100644 --- a/tox.ini +++ b/tox.ini @@ -4,11 +4,12 @@ # and then run "tox" from this directory. [tox] -envlist = py27, py34, py35, py36, pypy +envlist = py38, py39, py310, py311, py312, py313 [testenv] -commands = py.test --cov=. tests +commands = coverage run -m pytest tests deps = pytest groestlcoin_hash coverage + mypy>=1.0