New Upstream Release - python-mnemonic

Ready changes

Summary

Merged new upstream version: 0.20 (was: 0.19).

Resulting package

Built on 2023-07-13T14:53 (took 5m37s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases python3-mnemonic

Lintian Result

Diff

diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..34ed344
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,2 @@
+Marek Palatinus <marek@satoshilabs.com>
+Pavol Rusnak <stick@satoshilabs.com>
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
new file mode 100644
index 0000000..8bca025
--- /dev/null
+++ b/CHANGELOG.rst
@@ -0,0 +1,45 @@
+Changelog
+=========
+
+.. default-role:: code
+
+All notable changes to this project will be documented in this file.
+
+The format is based on `Keep a Changelog`_, and this project adheres to
+`Semantic Versioning`_.
+
+`0.20`_ - 2021-07-27
+---------------------
+
+.. _0.20: https://github.com/trezor/python-mnemonic/compare/v0.19...v0.20
+
+Added
+~~~~~
+
+- Type annotations
+- Support for testnet private keys
+
+Changed
+~~~~~~~
+
+- Project directory structure was cleaned up
+- Language on the `Mnemonic` object is remembered instead of repeatedly detecting
+
+Removed
+~~~~~~~
+
+- Support for Python 2.7 and 3.4 was dropped
+
+
+
+0.19 - 2019-10-01
+------------------
+
+Added
+~~~~~
+
+- Start of changelog
+
+
+.. _Keep a Changelog: https://keepachangelog.com/en/1.0.0/
+.. _Semantic Versioning: https://semver.org/spec/v2.0.0.html
diff --git a/MANIFEST.in b/MANIFEST.in
index 1aba38f..cc983b9 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1 +1,6 @@
-include LICENSE
+include tools/*
+graft src
+graft tests
+
+include AUTHORS LICENSE README.rst CHANGELOG.rst
+include tox.ini
diff --git a/PKG-INFO b/PKG-INFO
index a8c30ba..bae1ad0 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
-Metadata-Version: 1.1
+Metadata-Version: 1.2
 Name: mnemonic
-Version: 0.19
+Version: 0.20
 Summary: Implementation of Bitcoin BIP-0039
 Home-page: https://github.com/trezor/python-mnemonic
 Author: Trezor
@@ -9,8 +9,8 @@ License: UNKNOWN
 Description: python-mnemonic
         ===============
         
-        .. image:: https://travis-ci.org/trezor/python-mnemonic.svg?branch=master
-            :target: https://travis-ci.org/trezor/python-mnemonic
+        .. image:: https://badge.fury.io/py/mnemonic.svg
+            :target: https://badge.fury.io/py/mnemonic
         
         Reference implementation of BIP-0039: Mnemonic code for generating
         deterministic keys
@@ -31,10 +31,106 @@ Description: python-mnemonic
         See https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
         for full specification
         
+        Installation
+        ------------
+        
+        To install this library and its dependencies use:
+        
+         ``pip install mnemonic``
+        
+        Usage examples
+        --------------
+        
+        Import library into python project via:
+        
+        .. code-block:: python
+        
+           from mnemonic import Mnemonic
+        
+        Initialize class instance, picking from available dictionaries:
+        
+        - english
+        - chinese_simplified
+        - chinese_traditional
+        - french
+        - italian
+        - japanese
+        - korean 
+        - spanish
+        
+        .. code-block:: python
+        
+           mnemo = Mnemonic(language)
+           mnemo = Mnemonic("english")
+        
+        Generate word list given the strength (128 - 256):
+        
+        .. code-block:: python
+        
+           words = mnemo.generate(strength=256)
+          
+        Given the word list and custom passphrase (empty in example), generate seed:
+        
+        .. code-block:: python
+        
+           seed = mnemo.to_seed(words, passphrase="") 
+        
+        Given the word list, calculate original entropy:
+        
+        .. code-block:: python
+        
+           entropy = mnemo.to_entropy(words)
+        
+        Changelog
+        =========
+        
+        .. default-role:: code
+        
+        All notable changes to this project will be documented in this file.
+        
+        The format is based on `Keep a Changelog`_, and this project adheres to
+        `Semantic Versioning`_.
+        
+        `0.20`_ - 2021-07-27
+        ---------------------
+        
+        .. _0.20: https://github.com/trezor/python-mnemonic/compare/v0.19...v0.20
+        
+        Added
+        ~~~~~
+        
+        - Type annotations
+        - Support for testnet private keys
+        
+        Changed
+        ~~~~~~~
+        
+        - Project directory structure was cleaned up
+        - Language on the `Mnemonic` object is remembered instead of repeatedly detecting
+        
+        Removed
+        ~~~~~~~
+        
+        - Support for Python 2.7 and 3.4 was dropped
+        
+        
+        
+        0.19 - 2019-10-01
+        ------------------
+        
+        Added
+        ~~~~~
+        
+        - Start of changelog
+        
+        
+        .. _Keep a Changelog: https://keepachangelog.com/en/1.0.0/
+        .. _Semantic Versioning: https://semver.org/spec/v2.0.0.html
+        
 Platform: UNKNOWN
 Classifier: License :: OSI Approved :: MIT License
 Classifier: Operating System :: POSIX :: Linux
 Classifier: Operating System :: Microsoft :: Windows
 Classifier: Operating System :: MacOS :: MacOS X
-Classifier: Programming Language :: Python :: 2
 Classifier: Programming Language :: Python :: 3
+Requires-Python: >=3.5
diff --git a/README.rst b/README.rst
index 975343e..ec3c074 100644
--- a/README.rst
+++ b/README.rst
@@ -1,8 +1,8 @@
 python-mnemonic
 ===============
 
-.. image:: https://travis-ci.org/trezor/python-mnemonic.svg?branch=master
-    :target: https://travis-ci.org/trezor/python-mnemonic
+.. image:: https://badge.fury.io/py/mnemonic.svg
+    :target: https://badge.fury.io/py/mnemonic
 
 Reference implementation of BIP-0039: Mnemonic code for generating
 deterministic keys
@@ -22,3 +22,53 @@ BIP Paper
 
 See https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
 for full specification
+
+Installation
+------------
+
+To install this library and its dependencies use:
+
+ ``pip install mnemonic``
+
+Usage examples
+--------------
+
+Import library into python project via:
+
+.. code-block:: python
+
+   from mnemonic import Mnemonic
+
+Initialize class instance, picking from available dictionaries:
+
+- english
+- chinese_simplified
+- chinese_traditional
+- french
+- italian
+- japanese
+- korean 
+- spanish
+
+.. code-block:: python
+
+   mnemo = Mnemonic(language)
+   mnemo = Mnemonic("english")
+
+Generate word list given the strength (128 - 256):
+
+.. code-block:: python
+
+   words = mnemo.generate(strength=256)
+  
+Given the word list and custom passphrase (empty in example), generate seed:
+
+.. code-block:: python
+
+   seed = mnemo.to_seed(words, passphrase="") 
+
+Given the word list, calculate original entropy:
+
+.. code-block:: python
+
+   entropy = mnemo.to_entropy(words)
diff --git a/debian/changelog b/debian/changelog
index 902968f..da740e0 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+python-mnemonic (0.20-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Thu, 13 Jul 2023 14:48:14 -0000
+
 python-mnemonic (0.19-3) unstable; urgency=medium
 
   * Team upload.
diff --git a/mnemonic.egg-info/PKG-INFO b/mnemonic.egg-info/PKG-INFO
deleted file mode 100644
index a8c30ba..0000000
--- a/mnemonic.egg-info/PKG-INFO
+++ /dev/null
@@ -1,40 +0,0 @@
-Metadata-Version: 1.1
-Name: mnemonic
-Version: 0.19
-Summary: Implementation of Bitcoin BIP-0039
-Home-page: https://github.com/trezor/python-mnemonic
-Author: Trezor
-Author-email: info@trezor.io
-License: UNKNOWN
-Description: python-mnemonic
-        ===============
-        
-        .. image:: https://travis-ci.org/trezor/python-mnemonic.svg?branch=master
-            :target: https://travis-ci.org/trezor/python-mnemonic
-        
-        Reference implementation of BIP-0039: Mnemonic code for generating
-        deterministic keys
-        
-        Abstract
-        --------
-        
-        This BIP describes the implementation of a mnemonic code or mnemonic sentence --
-        a group of easy to remember words -- for the generation of deterministic wallets.
-        
-        It consists of two parts: generating the mnenomic, and converting it into a
-        binary seed. This seed can be later used to generate deterministic wallets using
-        BIP-0032 or similar methods.
-        
-        BIP Paper
-        ---------
-        
-        See https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
-        for full specification
-        
-Platform: UNKNOWN
-Classifier: License :: OSI Approved :: MIT License
-Classifier: Operating System :: POSIX :: Linux
-Classifier: Operating System :: Microsoft :: Windows
-Classifier: Operating System :: MacOS :: MacOS X
-Classifier: Programming Language :: Python :: 2
-Classifier: Programming Language :: Python :: 3
diff --git a/mnemonic.egg-info/SOURCES.txt b/mnemonic.egg-info/SOURCES.txt
deleted file mode 100644
index ddf7981..0000000
--- a/mnemonic.egg-info/SOURCES.txt
+++ /dev/null
@@ -1,20 +0,0 @@
-LICENSE
-MANIFEST.in
-README.rst
-setup.cfg
-setup.py
-mnemonic/__init__.py
-mnemonic/mnemonic.py
-mnemonic.egg-info/PKG-INFO
-mnemonic.egg-info/SOURCES.txt
-mnemonic.egg-info/dependency_links.txt
-mnemonic.egg-info/not-zip-safe
-mnemonic.egg-info/top_level.txt
-mnemonic/wordlist/chinese_simplified.txt
-mnemonic/wordlist/chinese_traditional.txt
-mnemonic/wordlist/english.txt
-mnemonic/wordlist/french.txt
-mnemonic/wordlist/italian.txt
-mnemonic/wordlist/japanese.txt
-mnemonic/wordlist/korean.txt
-mnemonic/wordlist/spanish.txt
\ No newline at end of file
diff --git a/mnemonic/__init__.py b/mnemonic/__init__.py
deleted file mode 100644
index 47e293d..0000000
--- a/mnemonic/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from .mnemonic import Mnemonic  # noqa: F401
diff --git a/setup.cfg b/setup.cfg
index adf5ed7..1aa827f 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,29 @@
-[bdist_wheel]
-universal = 1
+[flake8]
+extend-ignore = 
+	E203,
+	E501,
+	I900,
+
+[mypy]
+warn_unreachable = True
+warn_unused_ignores = True
+warn_redundant_casts = True
+warn_unused_configs = True
+incremental = False
+disallow_untyped_defs = True
+check_untyped_defs = True
+strict_equality = True
+implicit_reexport = False
+no_implicit_optional = True
+
+[mypy-generate_vectors]
+ignore_errors = True
+
+[mypy-setuptools.*]
+ignore_missing_imports = True
+
+[mypy-bip32utils.*]
+ignore_missing_imports = True
 
 [egg_info]
 tag_build = 
diff --git a/setup.py b/setup.py
index 47ee0c2..9cc27d7 100755
--- a/setup.py
+++ b/setup.py
@@ -1,33 +1,33 @@
-#!/usr/bin/env python
-import os
+#!/usr/bin/env python3
+from pathlib import Path
 from setuptools import setup
 
-CWD = os.path.dirname(os.path.realpath(__file__))
-
-
-def read(*path):
-    filename = os.path.join(CWD, *path)
-    with open(filename, "r") as f:
-        return f.read()
+CWD = Path(__file__).resolve().parent
 
 
 setup(
     name="mnemonic",
-    version="0.19",
+    version="0.20",
     author="Trezor",
     author_email="info@trezor.io",
     description="Implementation of Bitcoin BIP-0039",
-    long_description=read("README.rst"),
+    long_description="\n".join(
+        (
+            (CWD / "README.rst").read_text(),
+            (CWD / "CHANGELOG.rst").read_text(),
+        )
+    ),
     url="https://github.com/trezor/python-mnemonic",
     packages=["mnemonic"],
-    package_data={"mnemonic": ["wordlist/*.txt"]},
+    package_dir={"": "src"},
+    include_package_data=True,
     zip_safe=False,
+    python_requires=">=3.5",
     classifiers=[
         "License :: OSI Approved :: MIT License",
         "Operating System :: POSIX :: Linux",
         "Operating System :: Microsoft :: Windows",
         "Operating System :: MacOS :: MacOS X",
-        "Programming Language :: Python :: 2",
         "Programming Language :: Python :: 3",
     ],
 )
diff --git a/src/mnemonic.egg-info/PKG-INFO b/src/mnemonic.egg-info/PKG-INFO
new file mode 100644
index 0000000..bae1ad0
--- /dev/null
+++ b/src/mnemonic.egg-info/PKG-INFO
@@ -0,0 +1,136 @@
+Metadata-Version: 1.2
+Name: mnemonic
+Version: 0.20
+Summary: Implementation of Bitcoin BIP-0039
+Home-page: https://github.com/trezor/python-mnemonic
+Author: Trezor
+Author-email: info@trezor.io
+License: UNKNOWN
+Description: python-mnemonic
+        ===============
+        
+        .. image:: https://badge.fury.io/py/mnemonic.svg
+            :target: https://badge.fury.io/py/mnemonic
+        
+        Reference implementation of BIP-0039: Mnemonic code for generating
+        deterministic keys
+        
+        Abstract
+        --------
+        
+        This BIP describes the implementation of a mnemonic code or mnemonic sentence --
+        a group of easy to remember words -- for the generation of deterministic wallets.
+        
+        It consists of two parts: generating the mnenomic, and converting it into a
+        binary seed. This seed can be later used to generate deterministic wallets using
+        BIP-0032 or similar methods.
+        
+        BIP Paper
+        ---------
+        
+        See https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
+        for full specification
+        
+        Installation
+        ------------
+        
+        To install this library and its dependencies use:
+        
+         ``pip install mnemonic``
+        
+        Usage examples
+        --------------
+        
+        Import library into python project via:
+        
+        .. code-block:: python
+        
+           from mnemonic import Mnemonic
+        
+        Initialize class instance, picking from available dictionaries:
+        
+        - english
+        - chinese_simplified
+        - chinese_traditional
+        - french
+        - italian
+        - japanese
+        - korean 
+        - spanish
+        
+        .. code-block:: python
+        
+           mnemo = Mnemonic(language)
+           mnemo = Mnemonic("english")
+        
+        Generate word list given the strength (128 - 256):
+        
+        .. code-block:: python
+        
+           words = mnemo.generate(strength=256)
+          
+        Given the word list and custom passphrase (empty in example), generate seed:
+        
+        .. code-block:: python
+        
+           seed = mnemo.to_seed(words, passphrase="") 
+        
+        Given the word list, calculate original entropy:
+        
+        .. code-block:: python
+        
+           entropy = mnemo.to_entropy(words)
+        
+        Changelog
+        =========
+        
+        .. default-role:: code
+        
+        All notable changes to this project will be documented in this file.
+        
+        The format is based on `Keep a Changelog`_, and this project adheres to
+        `Semantic Versioning`_.
+        
+        `0.20`_ - 2021-07-27
+        ---------------------
+        
+        .. _0.20: https://github.com/trezor/python-mnemonic/compare/v0.19...v0.20
+        
+        Added
+        ~~~~~
+        
+        - Type annotations
+        - Support for testnet private keys
+        
+        Changed
+        ~~~~~~~
+        
+        - Project directory structure was cleaned up
+        - Language on the `Mnemonic` object is remembered instead of repeatedly detecting
+        
+        Removed
+        ~~~~~~~
+        
+        - Support for Python 2.7 and 3.4 was dropped
+        
+        
+        
+        0.19 - 2019-10-01
+        ------------------
+        
+        Added
+        ~~~~~
+        
+        - Start of changelog
+        
+        
+        .. _Keep a Changelog: https://keepachangelog.com/en/1.0.0/
+        .. _Semantic Versioning: https://semver.org/spec/v2.0.0.html
+        
+Platform: UNKNOWN
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Operating System :: POSIX :: Linux
+Classifier: Operating System :: Microsoft :: Windows
+Classifier: Operating System :: MacOS :: MacOS X
+Classifier: Programming Language :: Python :: 3
+Requires-Python: >=3.5
diff --git a/src/mnemonic.egg-info/SOURCES.txt b/src/mnemonic.egg-info/SOURCES.txt
new file mode 100644
index 0000000..fe1b166
--- /dev/null
+++ b/src/mnemonic.egg-info/SOURCES.txt
@@ -0,0 +1,26 @@
+AUTHORS
+CHANGELOG.rst
+LICENSE
+MANIFEST.in
+README.rst
+setup.cfg
+setup.py
+tox.ini
+src/mnemonic/__init__.py
+src/mnemonic/mnemonic.py
+src/mnemonic/py.typed
+src/mnemonic.egg-info/PKG-INFO
+src/mnemonic.egg-info/SOURCES.txt
+src/mnemonic.egg-info/dependency_links.txt
+src/mnemonic.egg-info/not-zip-safe
+src/mnemonic.egg-info/top_level.txt
+src/mnemonic/wordlist/chinese_simplified.txt
+src/mnemonic/wordlist/chinese_traditional.txt
+src/mnemonic/wordlist/english.txt
+src/mnemonic/wordlist/french.txt
+src/mnemonic/wordlist/italian.txt
+src/mnemonic/wordlist/japanese.txt
+src/mnemonic/wordlist/korean.txt
+src/mnemonic/wordlist/spanish.txt
+tests/test_mnemonic.py
+tools/generate_vectors.py
\ No newline at end of file
diff --git a/mnemonic.egg-info/dependency_links.txt b/src/mnemonic.egg-info/dependency_links.txt
similarity index 100%
rename from mnemonic.egg-info/dependency_links.txt
rename to src/mnemonic.egg-info/dependency_links.txt
diff --git a/mnemonic.egg-info/not-zip-safe b/src/mnemonic.egg-info/not-zip-safe
similarity index 100%
rename from mnemonic.egg-info/not-zip-safe
rename to src/mnemonic.egg-info/not-zip-safe
diff --git a/mnemonic.egg-info/top_level.txt b/src/mnemonic.egg-info/top_level.txt
similarity index 100%
rename from mnemonic.egg-info/top_level.txt
rename to src/mnemonic.egg-info/top_level.txt
diff --git a/src/mnemonic/__init__.py b/src/mnemonic/__init__.py
new file mode 100644
index 0000000..f8c9e1f
--- /dev/null
+++ b/src/mnemonic/__init__.py
@@ -0,0 +1,3 @@
+from .mnemonic import Mnemonic
+
+__all__ = ["Mnemonic"]
diff --git a/mnemonic/mnemonic.py b/src/mnemonic/mnemonic.py
similarity index 75%
rename from mnemonic/mnemonic.py
rename to src/mnemonic/mnemonic.py
index 935620a..e45111b 100644
--- a/mnemonic/mnemonic.py
+++ b/src/mnemonic/mnemonic.py
@@ -20,15 +20,15 @@
 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 #
 
-import binascii
 import bisect
 import hashlib
 import hmac
 import itertools
 import os
-import sys
+from typing import AnyStr, List, Optional, Sequence, TypeVar, Union
 import unicodedata
 
+_T = TypeVar("_T")
 PBKDF2_ROUNDS = 2048
 
 
@@ -37,20 +37,23 @@ class ConfigurationError(Exception):
 
 
 # From <https://stackoverflow.com/questions/212358/binary-search-bisection-in-python/2233940#2233940>
-def binary_search(a, x, lo=0, hi=None):  # can't use a to specify default for hi
+def binary_search(
+    a: Sequence[_T],
+    x: _T,
+    lo: int = 0,
+    hi: Optional[int] = None,  # can't use a to specify default for hi
+) -> int:
     hi = hi if hi is not None else len(a)  # hi defaults to len(a)
     pos = bisect.bisect_left(a, x, lo, hi)  # find insertion position
     return pos if pos != hi and a[pos] == x else -1  # don't walk off the end
 
 
 # Refactored code segments from <https://github.com/keis/base58>
-def b58encode(v):
+def b58encode(v: bytes) -> str:
     alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
 
     p, acc = 1, 0
     for c in reversed(v):
-        if sys.version < "3":
-            c = ord(c)
         acc += p * c
         p = p << 8
 
@@ -62,39 +65,36 @@ def b58encode(v):
 
 
 class Mnemonic(object):
-    def __init__(self, language):
+    def __init__(self, language: str):
+        self.language = language
         self.radix = 2048
-        if sys.version < "3":
-            with open("%s/%s.txt" % (self._get_directory(), language), "r") as f:
-                self.wordlist = [w.strip().decode("utf8") for w in f.readlines()]
-        else:
-            with open(
-                "%s/%s.txt" % (self._get_directory(), language), "r", encoding="utf-8"
-            ) as f:
-                self.wordlist = [w.strip() for w in f.readlines()]
+        with open(
+            "%s/%s.txt" % (self._get_directory(), language), "r", encoding="utf-8"
+        ) as f:
+            self.wordlist = [w.strip() for w in f.readlines()]
         if len(self.wordlist) != self.radix:
             raise ConfigurationError(
                 "Wordlist should contain %d words, but it contains %d words."
                 % (self.radix, len(self.wordlist))
             )
 
-    @classmethod
-    def _get_directory(cls):
+    @staticmethod
+    def _get_directory() -> str:
         return os.path.join(os.path.dirname(__file__), "wordlist")
 
     @classmethod
-    def list_languages(cls):
+    def list_languages(cls) -> List[str]:
         return [
             f.split(".")[0]
             for f in os.listdir(cls._get_directory())
             if f.endswith(".txt")
         ]
 
-    @classmethod
-    def normalize_string(cls, txt):
-        if isinstance(txt, str if sys.version < "3" else bytes):
+    @staticmethod
+    def normalize_string(txt: AnyStr) -> str:
+        if isinstance(txt, bytes):
             utxt = txt.decode("utf8")
-        elif isinstance(txt, unicode if sys.version < "3" else str):  # noqa: F821
+        elif isinstance(txt, str):
             utxt = txt
         else:
             raise TypeError("String value expected")
@@ -102,7 +102,7 @@ class Mnemonic(object):
         return unicodedata.normalize("NFKD", utxt)
 
     @classmethod
-    def detect_language(cls, code):
+    def detect_language(cls, code: str) -> str:
         code = cls.normalize_string(code)
         first = code.split(" ")[0]
         languages = cls.list_languages()
@@ -114,7 +114,7 @@ class Mnemonic(object):
 
         raise ConfigurationError("Language not detected")
 
-    def generate(self, strength=128):
+    def generate(self, strength: int = 128) -> str:
         if strength not in [128, 160, 192, 224, 256]:
             raise ValueError(
                 "Strength should be one of the following [128, 160, 192, 224, 256], but it is not (%d)."
@@ -123,7 +123,7 @@ class Mnemonic(object):
         return self.to_mnemonic(os.urandom(strength // 8))
 
     # Adapted from <http://tinyurl.com/oxmn476>
-    def to_entropy(self, words):
+    def to_entropy(self, words: Union[List[str], str]) -> bytearray:
         if not isinstance(words, list):
             words = words.split(" ")
         if len(words) not in [12, 15, 18, 21, 24]:
@@ -136,7 +136,7 @@ class Mnemonic(object):
         concatLenBits = len(words) * 11
         concatBits = [False] * concatLenBits
         wordindex = 0
-        if self.detect_language(" ".join(words)) == "english":
+        if self.language == "english":
             use_binary_search = True
         else:
             use_binary_search = False
@@ -163,28 +163,18 @@ class Mnemonic(object):
                     entropy[ii] |= 1 << (7 - jj)
         # Take the digest of the entropy.
         hashBytes = hashlib.sha256(entropy).digest()
-        if sys.version < "3":
-            hashBits = list(
-                itertools.chain.from_iterable(
-                    (
-                        [ord(c) & (1 << (7 - i)) != 0 for i in range(8)]
-                        for c in hashBytes
-                    )
-                )
-            )
-        else:
-            hashBits = list(
-                itertools.chain.from_iterable(
-                    ([c & (1 << (7 - i)) != 0 for i in range(8)] for c in hashBytes)
-                )
+        hashBits = list(
+            itertools.chain.from_iterable(
+                [c & (1 << (7 - i)) != 0 for i in range(8)] for c in hashBytes
             )
+        )
         # Check all the checksum bits.
         for i in range(checksumLengthBits):
             if concatBits[entropyLengthBits + i] != hashBits[i]:
                 raise ValueError("Failed checksum.")
         return entropy
 
-    def to_mnemonic(self, data):
+    def to_mnemonic(self, data: bytes) -> str:
         if len(data) not in [16, 20, 24, 28, 32]:
             raise ValueError(
                 "Data length should be one of the following: [16, 20, 24, 28, 32], but it is not (%d)."
@@ -192,39 +182,39 @@ class Mnemonic(object):
             )
         h = hashlib.sha256(data).hexdigest()
         b = (
-            bin(int(binascii.hexlify(data), 16))[2:].zfill(len(data) * 8)
+            bin(int.from_bytes(data, byteorder="big"))[2:].zfill(len(data) * 8)
             + bin(int(h, 16))[2:].zfill(256)[: len(data) * 8 // 32]
         )
         result = []
         for i in range(len(b) // 11):
             idx = int(b[i * 11 : (i + 1) * 11], 2)
             result.append(self.wordlist[idx])
-        if (
-            self.detect_language(" ".join(result)) == "japanese"
-        ):  # Japanese must be joined by ideographic space.
+        if self.language == "japanese":  # Japanese must be joined by ideographic space.
             result_phrase = u"\u3000".join(result)
         else:
             result_phrase = " ".join(result)
         return result_phrase
 
-    def check(self, mnemonic):
-        mnemonic = self.normalize_string(mnemonic).split(" ")
+    def check(self, mnemonic: str) -> bool:
+        mnemonic_list = self.normalize_string(mnemonic).split(" ")
         # list of valid mnemonic lengths
-        if len(mnemonic) not in [12, 15, 18, 21, 24]:
+        if len(mnemonic_list) not in [12, 15, 18, 21, 24]:
             return False
         try:
-            idx = map(lambda x: bin(self.wordlist.index(x))[2:].zfill(11), mnemonic)
+            idx = map(
+                lambda x: bin(self.wordlist.index(x))[2:].zfill(11), mnemonic_list
+            )
             b = "".join(idx)
         except ValueError:
             return False
         l = len(b)  # noqa: E741
         d = b[: l // 33 * 32]
         h = b[-l // 33 :]
-        nd = binascii.unhexlify(hex(int(d, 2))[2:].rstrip("L").zfill(l // 33 * 8))
+        nd = int(d, 2).to_bytes(l // 33 * 4, byteorder="big")
         nh = bin(int(hashlib.sha256(nd).hexdigest(), 16))[2:].zfill(256)[: l // 33]
         return h == nh
 
-    def expand_word(self, prefix):
+    def expand_word(self, prefix: str) -> str:
         if prefix in self.wordlist:
             return prefix
         else:
@@ -236,21 +226,23 @@ class Mnemonic(object):
                 # this is not a validation routine, just return the input
                 return prefix
 
-    def expand(self, mnemonic):
+    def expand(self, mnemonic: str) -> str:
         return " ".join(map(self.expand_word, mnemonic.split(" ")))
 
     @classmethod
-    def to_seed(cls, mnemonic, passphrase=""):
+    def to_seed(cls, mnemonic: str, passphrase: str = "") -> bytes:
         mnemonic = cls.normalize_string(mnemonic)
         passphrase = cls.normalize_string(passphrase)
         passphrase = "mnemonic" + passphrase
-        mnemonic = mnemonic.encode("utf-8")
-        passphrase = passphrase.encode("utf-8")
-        stretched = hashlib.pbkdf2_hmac("sha512", mnemonic, passphrase, PBKDF2_ROUNDS)
+        mnemonic_bytes = mnemonic.encode("utf-8")
+        passphrase_bytes = passphrase.encode("utf-8")
+        stretched = hashlib.pbkdf2_hmac(
+            "sha512", mnemonic_bytes, passphrase_bytes, PBKDF2_ROUNDS
+        )
         return stretched[:64]
 
-    @classmethod
-    def to_hd_master_key(cls, seed):
+    @staticmethod
+    def to_hd_master_key(seed: bytes, testnet: bool = False) -> str:
         if len(seed) != 64:
             raise ValueError("Provided seed should have length of 64")
 
@@ -259,6 +251,8 @@ class Mnemonic(object):
 
         # Serialization format can be found at: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#Serialization_format
         xprv = b"\x04\x88\xad\xe4"  # Version for private mainnet
+        if testnet:
+            xprv = b"\x04\x35\x83\x94"  # Version for private testnet
         xprv += b"\x00" * 9  # Depth, parent fingerprint, and child number
         xprv += seed[32:]  # Chain code
         xprv += b"\x00" + seed[:32]  # Master key
@@ -274,15 +268,14 @@ class Mnemonic(object):
         return b58encode(xprv)
 
 
-def main():
-    import binascii
+def main() -> None:
     import sys
 
     if len(sys.argv) > 1:
-        data = sys.argv[1]
+        hex_data = sys.argv[1]
     else:
-        data = sys.stdin.readline().strip()
-    data = binascii.unhexlify(data)
+        hex_data = sys.stdin.readline().strip()
+    data = bytes.fromhex(hex_data)
     m = Mnemonic("english")
     print(m.to_mnemonic(data))
 
diff --git a/src/mnemonic/py.typed b/src/mnemonic/py.typed
new file mode 100644
index 0000000..1242d43
--- /dev/null
+++ b/src/mnemonic/py.typed
@@ -0,0 +1 @@
+# Marker file for PEP 561.
diff --git a/mnemonic/wordlist/chinese_simplified.txt b/src/mnemonic/wordlist/chinese_simplified.txt
similarity index 100%
rename from mnemonic/wordlist/chinese_simplified.txt
rename to src/mnemonic/wordlist/chinese_simplified.txt
diff --git a/mnemonic/wordlist/chinese_traditional.txt b/src/mnemonic/wordlist/chinese_traditional.txt
similarity index 100%
rename from mnemonic/wordlist/chinese_traditional.txt
rename to src/mnemonic/wordlist/chinese_traditional.txt
diff --git a/mnemonic/wordlist/english.txt b/src/mnemonic/wordlist/english.txt
similarity index 100%
rename from mnemonic/wordlist/english.txt
rename to src/mnemonic/wordlist/english.txt
diff --git a/mnemonic/wordlist/french.txt b/src/mnemonic/wordlist/french.txt
similarity index 99%
rename from mnemonic/wordlist/french.txt
rename to src/mnemonic/wordlist/french.txt
index 8600949..bcf7942 100644
--- a/mnemonic/wordlist/french.txt
+++ b/src/mnemonic/wordlist/french.txt
@@ -1,4 +1,4 @@
-abaisser
+abaisser
 abandon
 abdiquer
 abeille
diff --git a/mnemonic/wordlist/italian.txt b/src/mnemonic/wordlist/italian.txt
similarity index 100%
rename from mnemonic/wordlist/italian.txt
rename to src/mnemonic/wordlist/italian.txt
diff --git a/mnemonic/wordlist/japanese.txt b/src/mnemonic/wordlist/japanese.txt
similarity index 100%
rename from mnemonic/wordlist/japanese.txt
rename to src/mnemonic/wordlist/japanese.txt
diff --git a/mnemonic/wordlist/korean.txt b/src/mnemonic/wordlist/korean.txt
similarity index 100%
rename from mnemonic/wordlist/korean.txt
rename to src/mnemonic/wordlist/korean.txt
diff --git a/mnemonic/wordlist/spanish.txt b/src/mnemonic/wordlist/spanish.txt
similarity index 100%
rename from mnemonic/wordlist/spanish.txt
rename to src/mnemonic/wordlist/spanish.txt
diff --git a/tests/test_mnemonic.py b/tests/test_mnemonic.py
new file mode 100755
index 0000000..959c238
--- /dev/null
+++ b/tests/test_mnemonic.py
@@ -0,0 +1,127 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2013 Pavol Rusnak
+# Copyright (c) 2017 mruddy
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy of
+# this software and associated documentation files (the "Software"), to deal in
+# the Software without restriction, including without limitation the rights to
+# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+# of the Software, and to permit persons to whom the Software is furnished to do
+# so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+import json
+import random
+import unittest
+from typing import List
+
+from mnemonic import Mnemonic
+
+
+class MnemonicTest(unittest.TestCase):
+    def _check_list(self, language: str, vectors: List[str]) -> None:
+        mnemo = Mnemonic(language)
+        for v in vectors:
+            code = mnemo.to_mnemonic(bytes.fromhex(v[0]))
+            seed = Mnemonic.to_seed(code, passphrase="TREZOR")
+            xprv = Mnemonic.to_hd_master_key(seed)
+            self.assertIs(mnemo.check(v[1]), True)
+            self.assertEqual(v[1], code)
+            self.assertEqual(v[2], seed.hex())
+            self.assertEqual(v[3], xprv)
+
+    def test_vectors(self) -> None:
+        with open("vectors.json", "r") as f:
+            vectors = json.load(f)
+        for lang in vectors.keys():
+            self._check_list(lang, vectors[lang])
+
+    def test_failed_checksum(self) -> None:
+        code = (
+            "bless cloud wheel regular tiny venue bird web grief security dignity zoo"
+        )
+        mnemo = Mnemonic("english")
+        self.assertFalse(mnemo.check(code))
+
+    def test_detection(self) -> None:
+        self.assertEqual("english", Mnemonic.detect_language("security"))
+
+        with self.assertRaises(Exception):
+            Mnemonic.detect_language("xxxxxxx")
+
+    def test_utf8_nfkd(self) -> None:
+        # The same sentence in various UTF-8 forms
+        words_nfkd = u"Pr\u030ci\u0301s\u030cerne\u030c z\u030clut\u030couc\u030cky\u0301 ku\u030an\u030c u\u0301pe\u030cl d\u030ca\u0301belske\u0301 o\u0301dy za\u0301ker\u030cny\u0301 uc\u030cen\u030c be\u030cz\u030ci\u0301 pode\u0301l zo\u0301ny u\u0301lu\u030a"
+        words_nfc = u"P\u0159\xed\u0161ern\u011b \u017elu\u0165ou\u010dk\xfd k\u016f\u0148 \xfap\u011bl \u010f\xe1belsk\xe9 \xf3dy z\xe1ke\u0159n\xfd u\u010de\u0148 b\u011b\u017e\xed pod\xe9l z\xf3ny \xfal\u016f"
+        words_nfkc = u"P\u0159\xed\u0161ern\u011b \u017elu\u0165ou\u010dk\xfd k\u016f\u0148 \xfap\u011bl \u010f\xe1belsk\xe9 \xf3dy z\xe1ke\u0159n\xfd u\u010de\u0148 b\u011b\u017e\xed pod\xe9l z\xf3ny \xfal\u016f"
+        words_nfd = u"Pr\u030ci\u0301s\u030cerne\u030c z\u030clut\u030couc\u030cky\u0301 ku\u030an\u030c u\u0301pe\u030cl d\u030ca\u0301belske\u0301 o\u0301dy za\u0301ker\u030cny\u0301 uc\u030cen\u030c be\u030cz\u030ci\u0301 pode\u0301l zo\u0301ny u\u0301lu\u030a"
+
+        passphrase_nfkd = (
+            u"Neuve\u030cr\u030citelne\u030c bezpec\u030cne\u0301 hesli\u0301c\u030cko"
+        )
+        passphrase_nfc = (
+            u"Neuv\u011b\u0159iteln\u011b bezpe\u010dn\xe9 hesl\xed\u010dko"
+        )
+        passphrase_nfkc = (
+            u"Neuv\u011b\u0159iteln\u011b bezpe\u010dn\xe9 hesl\xed\u010dko"
+        )
+        passphrase_nfd = (
+            u"Neuve\u030cr\u030citelne\u030c bezpec\u030cne\u0301 hesli\u0301c\u030cko"
+        )
+
+        seed_nfkd = Mnemonic.to_seed(words_nfkd, passphrase_nfkd)
+        seed_nfc = Mnemonic.to_seed(words_nfc, passphrase_nfc)
+        seed_nfkc = Mnemonic.to_seed(words_nfkc, passphrase_nfkc)
+        seed_nfd = Mnemonic.to_seed(words_nfd, passphrase_nfd)
+
+        self.assertEqual(seed_nfkd, seed_nfc)
+        self.assertEqual(seed_nfkd, seed_nfkc)
+        self.assertEqual(seed_nfkd, seed_nfd)
+
+    def test_to_entropy(self) -> None:
+        data = [bytes(random.getrandbits(8) for _ in range(32)) for _ in range(1024)]
+        data.append(b"Lorem ipsum dolor sit amet amet.")
+        m = Mnemonic("english")
+        for d in data:
+            self.assertEqual(m.to_entropy(m.to_mnemonic(d).split()), d)
+
+    def test_expand_word(self) -> None:
+        m = Mnemonic("english")
+        self.assertEqual("", m.expand_word(""))
+        self.assertEqual(" ", m.expand_word(" "))
+        self.assertEqual("access", m.expand_word("access"))  # word in list
+        self.assertEqual(
+            "access", m.expand_word("acce")
+        )  # unique prefix expanded to word in list
+        self.assertEqual("acb", m.expand_word("acb"))  # not found at all
+        self.assertEqual("acc", m.expand_word("acc"))  # multi-prefix match
+        self.assertEqual("act", m.expand_word("act"))  # exact three letter match
+        self.assertEqual(
+            "action", m.expand_word("acti")
+        )  # unique prefix expanded to word in list
+
+    def test_expand(self) -> None:
+        m = Mnemonic("english")
+        self.assertEqual("access", m.expand("access"))
+        self.assertEqual(
+            "access access acb acc act action", m.expand("access acce acb acc act acti")
+        )
+
+
+def __main__() -> None:
+    unittest.main()
+
+
+if __name__ == "__main__":
+    __main__()
diff --git a/tools/generate_vectors.py b/tools/generate_vectors.py
new file mode 100755
index 0000000..f03955a
--- /dev/null
+++ b/tools/generate_vectors.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python3
+
+import json
+from random import choice, seed
+
+from bip32utils import BIP32Key
+from mnemonic import Mnemonic
+
+
+def process(data, lst):
+    code = mnemo.to_mnemonic(bytes.fromhex(data))
+    seed = Mnemonic.to_seed(code, passphrase="TREZOR")
+    xprv = BIP32Key.fromEntropy(seed).ExtendedKey()
+    seed = seed.hex()
+    print("input    : %s (%d bits)" % (data, len(data) * 4))
+    print("mnemonic : %s (%d words)" % (code, len(code.split(" "))))
+    print("seed     : %s (%d bits)" % (seed, len(seed) * 4))
+    print("xprv     : %s" % xprv)
+    print()
+    lst.append((data, code, seed, xprv))
+
+
+if __name__ == "__main__":
+    out = {}
+    seed(1337)
+
+    for lang in ["english"]:  # Mnemonic.list_languages():
+        mnemo = Mnemonic(lang)
+        out[lang] = []
+
+        # Generate corner cases
+        data = []
+        for length in range(16, 32 + 1, 8):
+            for b in ["00", "7f", "80", "ff"]:
+                process(b * length, out[lang])
+
+        # Generate random seeds
+        for i in range(12):
+            data = "".join(chr(choice(range(0, 256))) for _ in range(8 * (i % 3 + 2)))
+            data = data.encode("latin1")
+            process(data.hex(), out[lang])
+
+    with open("vectors.json", "w") as f:
+        json.dump(
+            out, f, sort_keys=True, indent=4, separators=(",", ": "), ensure_ascii=False
+        )
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..87ea5b2
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,13 @@
+[tox]
+envlist =
+    py35,
+    py36,
+    py37,
+    py38,
+    py39,
+    py310,
+    pypy,
+
+[testenv]
+commands =
+    python tests/test_mnemonic.py

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/lib/python3/dist-packages/mnemonic-0.20.egg-info/PKG-INFO
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/mnemonic-0.20.egg-info/dependency_links.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/mnemonic-0.20.egg-info/not-zip-safe
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/mnemonic-0.20.egg-info/top_level.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/mnemonic/py.typed
-rw-r--r--  root/root   /usr/share/doc/python3-mnemonic/changelog.gz

Files in first set of .debs but not in second

-rw-r--r--  root/root   /usr/lib/python3/dist-packages/mnemonic-0.19.egg-info/PKG-INFO
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/mnemonic-0.19.egg-info/dependency_links.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/mnemonic-0.19.egg-info/not-zip-safe
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/mnemonic-0.19.egg-info/top_level.txt

No differences were encountered in the control files

More details

Full run details