diff --git a/pyproject.toml b/pyproject.toml index 4262af87..d49a14fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,15 @@ warn_unreachable = true no_implicit_reexport = true strict_equality = true -files = ["src/nacl"] +# Include test files manually for now. Can tidy up later. +files = [ + "src/nacl", + "tests/test_aead.py", + "tests/test_bindings.py", + "tests/test_box.py", + "tests/test_signing.py", + "tests/utils.py", +] [[tool.mypy.overrides]] module = [ @@ -51,7 +59,7 @@ ignore_missing_imports = true # nacl._sodium return `Any` as far as mypy is concerned. It's not worth it to # stub the C functions or cast() their uses. But this means there are more # `Any`s floating around. So the more restrictive any checks we'd like to use -# should only be turned out outside of `bindings`. +# should only be turned on outside of `bindings`. [[tool.mypy.overrides]] module = [ @@ -60,3 +68,23 @@ module = [ disallow_any_expr = false warn_return_any = false +# Loosen some of the checks within the tests. +# For now this is an explicit list rather than a wildcard "test.*", to make +# it a little easier to run the strict checks on modules first. We can clean +# this up later. Note that we _do_ run the strict checks on `test.utils`. + +[[tool.mypy.overrides]] +module = [ + "tests.test_aead", + "tests.test_bindings", + "tests.test_box", + "tests.test_signing", +] +# Some library helpers types' involve `Any`, in particular `pytest.mark.parameterize` +# and `hypothesis.strategies.sampledfrom`. +disallow_any_expr = false +disallow_any_decorated = false + +# It's not useful to annotate each test function as `-> None`. +disallow_untyped_defs = false +disallow_incomplete_defs = false diff --git a/src/nacl/bindings/crypto_scalarmult.py b/src/nacl/bindings/crypto_scalarmult.py index 50155015..ca4a2819 100644 --- a/src/nacl/bindings/crypto_scalarmult.py +++ b/src/nacl/bindings/crypto_scalarmult.py @@ -20,8 +20,8 @@ has_crypto_scalarmult_ed25519 = bool(lib.PYNACL_HAS_CRYPTO_SCALARMULT_ED25519) -crypto_scalarmult_BYTES = lib.crypto_scalarmult_bytes() -crypto_scalarmult_SCALARBYTES = lib.crypto_scalarmult_scalarbytes() +crypto_scalarmult_BYTES: int = lib.crypto_scalarmult_bytes() +crypto_scalarmult_SCALARBYTES: int = lib.crypto_scalarmult_scalarbytes() crypto_scalarmult_ed25519_BYTES = 0 crypto_scalarmult_ed25519_SCALARBYTES = 0 diff --git a/tests/test_aead.py b/tests/test_aead.py index d67149e5..3b9e6920 100644 --- a/tests/test_aead.py +++ b/tests/test_aead.py @@ -11,10 +11,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - - import binascii -from collections import namedtuple +from typing import Callable, Dict, List, NamedTuple, Optional from hypothesis import given, settings from hypothesis.strategies import binary, sampled_from @@ -27,28 +25,32 @@ from .utils import read_kv_test_vectors -def chacha20poly1305_agl_vectors(): +def chacha20poly1305_agl_vectors() -> List[Dict[str, bytes]]: # NIST vectors derived format DATA = "chacha20-poly1305-agl_ref.txt" return read_kv_test_vectors(DATA, delimiter=b":", newrecord=b"AEAD") -def chacha20poly1305_ietf_vectors(): +def chacha20poly1305_ietf_vectors() -> List[Dict[str, bytes]]: # NIST vectors derived format DATA = "chacha20-poly1305-ietf_ref.txt" return read_kv_test_vectors(DATA, delimiter=b":", newrecord=b"AEAD") -def xchacha20poly1305_ietf_vectors(): +def xchacha20poly1305_ietf_vectors() -> List[Dict[str, bytes]]: # NIST vectors derived format DATA = "xchacha20-poly1305-ietf_ref.txt" return read_kv_test_vectors(DATA, delimiter=b":", newrecord=b"AEAD") -Construction = namedtuple("Construction", "encrypt, decrypt, NPUB, KEYBYTES") +class Construction(NamedTuple): + encrypt: Callable[[bytes, Optional[bytes], bytes, bytes], bytes] + decrypt: Callable[[bytes, Optional[bytes], bytes, bytes], bytes] + NPUB: int + KEYBYTES: int -def _getconstruction(construction): +def _getconstruction(construction: bytes) -> Construction: if construction == b"chacha20-poly1305-old": encrypt = b.crypto_aead_chacha20poly1305_encrypt decrypt = b.crypto_aead_chacha20poly1305_decrypt @@ -74,7 +76,7 @@ def _getconstruction(construction): + chacha20poly1305_ietf_vectors() + xchacha20poly1305_ietf_vectors(), ) -def test_chacha20poly1305_variants_kat(kv): +def test_chacha20poly1305_variants_kat(kv: Dict[str, bytes]): msg = binascii.unhexlify(kv["IN"]) ad = binascii.unhexlify(kv["AD"]) nonce = binascii.unhexlify(kv["NONCE"]) @@ -103,7 +105,7 @@ def test_chacha20poly1305_variants_kat(kv): ) @settings(deadline=None, max_examples=20) def test_chacha20poly1305_variants_roundtrip( - construction, message, aad, nonce, key + construction: bytes, message: bytes, aad: bytes, nonce: bytes, key: bytes ): c = _getconstruction(construction) @@ -123,30 +125,35 @@ def test_chacha20poly1305_variants_roundtrip( "construction", [b"chacha20-poly1305-old", b"chacha20-poly1305", b"xchacha20-poly1305"], ) -def test_chacha20poly1305_variants_wrong_params(construction): +def test_chacha20poly1305_variants_wrong_params(construction: bytes): c = _getconstruction(construction) nonce = b"\x00" * c.NPUB key = b"\x00" * c.KEYBYTES aad = None c.encrypt(b"", aad, nonce, key) + # The first two checks call encrypt with a nonce/key that's too short. Otherwise, + # the types are fine. (TODO: Should this raise ValueError rather than TypeError? + # Doing so would be a breaking change.) with pytest.raises(exc.TypeError): c.encrypt(b"", aad, nonce[:-1], key) with pytest.raises(exc.TypeError): c.encrypt(b"", aad, nonce, key[:-1]) + # Type safety: mypy spots these next two errors, but we want to check that they're + # spotted at runtime too. with pytest.raises(exc.TypeError): - c.encrypt(b"", aad, nonce.decode("utf-8"), key) + c.encrypt(b"", aad, nonce.decode("utf-8"), key) # type: ignore[arg-type] with pytest.raises(exc.TypeError): - c.encrypt(b"", aad, nonce, key.decode("utf-8")) + c.encrypt(b"", aad, nonce, key.decode("utf-8")) # type: ignore[arg-type] @pytest.mark.parametrize( "construction", [b"chacha20-poly1305-old", b"chacha20-poly1305", b"xchacha20-poly1305"], ) -def test_chacha20poly1305_variants_str_msg(construction): +def test_chacha20poly1305_variants_str_msg(construction: bytes): c = _getconstruction(construction) nonce = b"\x00" * c.NPUB key = b"\x00" * c.KEYBYTES aad = None with pytest.raises(exc.TypeError): - c.encrypt("", aad, nonce, key) + c.encrypt("", aad, nonce, key) # type: ignore[arg-type] diff --git a/tests/test_bindings.py b/tests/test_bindings.py index ef61cb4c..3bb726ff 100644 --- a/tests/test_bindings.py +++ b/tests/test_bindings.py @@ -15,6 +15,7 @@ import hashlib from binascii import hexlify, unhexlify +from typing import List, Tuple from hypothesis import given, settings from hypothesis.strategies import binary, integers @@ -28,7 +29,7 @@ from .utils import flip_byte, read_crypto_test_vectors -def tohex(b): +def tohex(b: bytes) -> str: return hexlify(b).decode("ascii") @@ -119,20 +120,22 @@ def test_box(): def test_box_wrong_lengths(): A_pubkey, A_secretkey = c.crypto_box_keypair() with pytest.raises(ValueError): - c.crypto_box(b"abc", "\x00", A_pubkey, A_secretkey) + c.crypto_box(b"abc", b"\x00", A_pubkey, A_secretkey) with pytest.raises(ValueError): c.crypto_box( - b"abc", "\x00" * c.crypto_box_NONCEBYTES, b"", A_secretkey + b"abc", b"\x00" * c.crypto_box_NONCEBYTES, b"", A_secretkey ) with pytest.raises(ValueError): - c.crypto_box(b"abc", "\x00" * c.crypto_box_NONCEBYTES, A_pubkey, b"") + c.crypto_box(b"abc", b"\x00" * c.crypto_box_NONCEBYTES, A_pubkey, b"") with pytest.raises(ValueError): c.crypto_box_open(b"", b"", b"", b"") with pytest.raises(ValueError): - c.crypto_box_open(b"", "\x00" * c.crypto_box_NONCEBYTES, b"", b"") + c.crypto_box_open(b"", b"\x00" * c.crypto_box_NONCEBYTES, b"", b"") with pytest.raises(ValueError): - c.crypto_box_open(b"", "\x00" * c.crypto_box_NONCEBYTES, A_pubkey, b"") + c.crypto_box_open( + b"", b"\x00" * c.crypto_box_NONCEBYTES, A_pubkey, b"" + ) with pytest.raises(ValueError): c.crypto_box_beforenm(b"", b"") @@ -173,7 +176,7 @@ def test_sign_wrong_lengths(): c.crypto_sign_seed_keypair(b"") -def secret_scalar(): +def secret_scalar() -> Tuple[bytes, bytes]: pubkey, secretkey = c.crypto_box_keypair() assert len(secretkey) == c.crypto_box_SECRETKEYBYTES assert c.crypto_box_SECRETKEYBYTES == c.crypto_scalarmult_BYTES @@ -272,17 +275,18 @@ def test_box_seal_wrong_lengths(): def test_box_seal_wrong_types(): A_pubkey, A_secretkey = c.crypto_box_keypair() + # type safety: mypy can spot these errors, but we want to spot them at runtime too. with pytest.raises(TypeError): - c.crypto_box_seal(b"abc", dict()) + c.crypto_box_seal(b"abc", dict()) # type: ignore[arg-type] with pytest.raises(TypeError): - c.crypto_box_seal_open(b"abc", None, A_secretkey) + c.crypto_box_seal_open(b"abc", None, A_secretkey) # type: ignore[arg-type] with pytest.raises(TypeError): - c.crypto_box_seal_open(b"abc", A_pubkey, None) + c.crypto_box_seal_open(b"abc", A_pubkey, None) # type: ignore[arg-type] with pytest.raises(TypeError): - c.crypto_box_seal_open(None, A_pubkey, A_secretkey) + c.crypto_box_seal_open(None, A_pubkey, A_secretkey) # type: ignore[arg-type] -def _box_from_seed_vectors(): +def _box_from_seed_vectors() -> List[Tuple[bytes, bytes, bytes]]: # Fmt: || DATA = "box_from_seed.txt" lines = read_crypto_test_vectors(DATA, maxels=2, delimiter=b"\t") @@ -299,7 +303,9 @@ def _box_from_seed_vectors(): @pytest.mark.parametrize( ("seed", "public_key", "secret_key"), _box_from_seed_vectors() ) -def test_box_seed_keypair_reference(seed, public_key, secret_key): +def test_box_seed_keypair_reference( + seed: bytes, public_key: bytes, secret_key: bytes +): seed = unhexlify(seed) pk, sk = c.crypto_box_seed_keypair(seed) assert pk == unhexlify(public_key) @@ -336,7 +342,7 @@ def test_unpad_not_padded(): binary(min_size=0, max_size=2049), integers(min_value=16, max_value=256) ) @settings(max_examples=20) -def test_pad_sizes(msg, bl_sz): +def test_pad_sizes(msg: bytes, bl_sz: int): padded = c.sodium_pad(msg, bl_sz) assert len(padded) > len(msg) assert len(padded) >= bl_sz @@ -347,7 +353,7 @@ def test_pad_sizes(msg, bl_sz): binary(min_size=0, max_size=2049), integers(min_value=16, max_value=256) ) @settings(max_examples=20) -def test_pad_roundtrip(msg, bl_sz): +def test_pad_roundtrip(msg: bytes, bl_sz: int): padded = c.sodium_pad(msg, bl_sz) assert len(padded) > len(msg) assert len(padded) >= bl_sz diff --git a/tests/test_box.py b/tests/test_box.py index bb6e43ce..69e36ddf 100644 --- a/tests/test_box.py +++ b/tests/test_box.py @@ -23,7 +23,7 @@ from nacl.utils import random from .test_bindings import _box_from_seed_vectors - +from .utils import check_type_error VECTORS = [ # privalice, pubalice, privbob, pubbob, nonce, plaintext, ciphertext @@ -61,7 +61,9 @@ def test_generate_private_key_from_random_seed(): @pytest.mark.parametrize( ("seed", "public_key", "secret_key"), _box_from_seed_vectors() ) -def test_generate_private_key_from_seed(seed, public_key, secret_key): +def test_generate_private_key_from_seed( + seed: bytes, public_key: bytes, secret_key: bytes +): prvt = PrivateKey.from_seed(seed, encoder=HexEncoder) sk = binascii.unhexlify(secret_key) pk = binascii.unhexlify(public_key) @@ -121,12 +123,18 @@ def test_box_bytes(): VECTORS, ) def test_box_encryption( - privalice, pubalice, privbob, pubbob, nonce, plaintext, ciphertext + privalice: bytes, + pubalice: bytes, + privbob: bytes, + pubbob: bytes, + nonce: bytes, + plaintext: bytes, + ciphertext: bytes, ): - pubalice = PublicKey(pubalice, encoder=HexEncoder) - privbob = PrivateKey(privbob, encoder=HexEncoder) + pubalice_decoded = PublicKey(pubalice, encoder=HexEncoder) + privbob_decoded = PrivateKey(privbob, encoder=HexEncoder) - box = Box(privbob, pubalice) + box = Box(privbob_decoded, pubalice_decoded) encrypted = box.encrypt( binascii.unhexlify(plaintext), binascii.unhexlify(nonce), @@ -155,12 +163,18 @@ def test_box_encryption( VECTORS, ) def test_box_decryption( - privalice, pubalice, privbob, pubbob, nonce, plaintext, ciphertext + privalice: bytes, + pubalice: bytes, + privbob: bytes, + pubbob: bytes, + nonce: bytes, + plaintext: bytes, + ciphertext: bytes, ): - pubbob = PublicKey(pubbob, encoder=HexEncoder) - privalice = PrivateKey(privalice, encoder=HexEncoder) + pubbob_decoded = PublicKey(pubbob, encoder=HexEncoder) + privalice_decoded = PrivateKey(privalice, encoder=HexEncoder) - box = Box(privalice, pubbob) + box = Box(privalice_decoded, pubbob_decoded) nonce = binascii.unhexlify(nonce) decrypted = binascii.hexlify( @@ -183,12 +197,18 @@ def test_box_decryption( VECTORS, ) def test_box_decryption_combined( - privalice, pubalice, privbob, pubbob, nonce, plaintext, ciphertext + privalice: bytes, + pubalice: bytes, + privbob: bytes, + pubbob: bytes, + nonce: bytes, + plaintext: bytes, + ciphertext: bytes, ): - pubbob = PublicKey(pubbob, encoder=HexEncoder) - privalice = PrivateKey(privalice, encoder=HexEncoder) + pubbob_decoded = PublicKey(pubbob, encoder=HexEncoder) + privalice_decoded = PrivateKey(privalice, encoder=HexEncoder) - box = Box(privalice, pubbob) + box = Box(privalice_decoded, pubbob_decoded) combined = binascii.hexlify( binascii.unhexlify(nonce) + binascii.unhexlify(ciphertext), @@ -211,12 +231,18 @@ def test_box_decryption_combined( VECTORS, ) def test_box_optional_nonce( - privalice, pubalice, privbob, pubbob, nonce, plaintext, ciphertext + privalice: bytes, + pubalice: bytes, + privbob: bytes, + pubbob: bytes, + nonce: bytes, + plaintext: bytes, + ciphertext: bytes, ): - pubbob = PublicKey(pubbob, encoder=HexEncoder) - privalice = PrivateKey(privalice, encoder=HexEncoder) + pubbob_decoded = PublicKey(pubbob, encoder=HexEncoder) + privalice_decoded = PrivateKey(privalice, encoder=HexEncoder) - box = Box(privalice, pubbob) + box = Box(privalice_decoded, pubbob_decoded) encrypted = box.encrypt(binascii.unhexlify(plaintext), encoder=HexEncoder) @@ -238,12 +264,18 @@ def test_box_optional_nonce( VECTORS, ) def test_box_encryption_generates_different_nonces( - privalice, pubalice, privbob, pubbob, nonce, plaintext, ciphertext + privalice: bytes, + pubalice: bytes, + privbob: bytes, + pubbob: bytes, + nonce: bytes, + plaintext: bytes, + ciphertext: bytes, ): - pubbob = PublicKey(pubbob, encoder=HexEncoder) - privalice = PrivateKey(privalice, encoder=HexEncoder) + pubbob_decoded = PublicKey(pubbob, encoder=HexEncoder) + privalice_decoded = PrivateKey(privalice, encoder=HexEncoder) - box = Box(privalice, pubbob) + box = Box(privalice_decoded, pubbob_decoded) nonce_0 = box.encrypt( binascii.unhexlify(plaintext), encoder=HexEncoder @@ -269,14 +301,20 @@ def test_box_encryption_generates_different_nonces( VECTORS, ) def test_box_failed_decryption( - privalice, pubalice, privbob, pubbob, nonce, plaintext, ciphertext + privalice: bytes, + pubalice: bytes, + privbob: bytes, + pubbob: bytes, + nonce: bytes, + plaintext: bytes, + ciphertext: bytes, ): - pubbob = PublicKey(pubbob, encoder=HexEncoder) - privbob = PrivateKey(privbob, encoder=HexEncoder) + pubbob_decoded = PublicKey(pubbob, encoder=HexEncoder) + privbob_decoded = PrivateKey(privbob, encoder=HexEncoder) # this cannot decrypt the ciphertext! the ciphertext must be decrypted by # (privalice, pubbob) or (privbob, pubalice) - box = Box(privbob, pubbob) + box = Box(privbob_decoded, pubbob_decoded) with pytest.raises(CryptoError): box.decrypt(ciphertext, binascii.unhexlify(nonce), encoder=HexEncoder) @@ -285,6 +323,7 @@ def test_box_failed_decryption( def test_box_wrong_length(): with pytest.raises(ValueError): PublicKey(b"") + # TODO: should the below raise a ValueError? with pytest.raises(TypeError): PrivateKey(b"") @@ -303,12 +342,6 @@ def test_box_wrong_length(): b.decrypt(b"", b"") -def check_type_error(expected, f, *args): - with pytest.raises(TypeError) as e: - f(*args) - assert expected in str(e.value) - - def test_wrong_types(): priv = PrivateKey.generate() diff --git a/tests/test_sealed_box.py b/tests/test_sealed_box.py index d4eabe69..874f5a36 100644 --- a/tests/test_sealed_box.py +++ b/tests/test_sealed_box.py @@ -21,7 +21,7 @@ from nacl.exceptions import CryptoError from nacl.public import PrivateKey, PublicKey, SealedBox -from .utils import read_crypto_test_vectors +from .utils import check_type_error, read_crypto_test_vectors def sealbox_vectors(): @@ -97,12 +97,6 @@ def test_sealed_box_decryption(privalice, pubalice, plaintext, encrypted): assert binascii.hexlify(decrypted) == plaintext -def check_type_error(expected, f, *args): - with pytest.raises(TypeError) as e: - f(*args) - assert expected in str(e.value) - - def test_wrong_types(): priv = PrivateKey.generate() diff --git a/tests/test_signing.py b/tests/test_signing.py index 6d10c51c..0eee523a 100644 --- a/tests/test_signing.py +++ b/tests/test_signing.py @@ -11,9 +11,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - - import binascii +from typing import List, Tuple, Union import pytest @@ -22,14 +21,19 @@ from nacl.exceptions import BadSignatureError from nacl.signing import SignedMessage, SigningKey, VerifyKey -from .utils import assert_equal, assert_not_equal, read_crypto_test_vectors +from .utils import ( + assert_equal, + assert_not_equal, + check_type_error, + read_crypto_test_vectors, +) -def tohex(b): +def tohex(b: bytes) -> str: return binascii.hexlify(b).decode("ascii") -def ed25519_known_answers(): +def ed25519_known_answers() -> List[Tuple[bytes, bytes, bytes, bytes, bytes]]: # Known answers taken from: http://ed25519.cr.yp.to/python/sign.input # hex-encoded fields on each input line: sk||pk, pk, msg, signature||msg # known answer fields: sk, pk, msg, signature, signed @@ -79,7 +83,7 @@ def test_equal_keys_have_equal_hashes(self): SigningKey(b"\x00" * (crypto_sign_SEEDBYTES - 1) + b"\x01"), ], ) - def test_different_keys_are_not_equal(self, k2): + def test_different_keys_are_not_equal(self, k2: Union[bytes, SigningKey]): k1 = SigningKey(b"\x00" * crypto_sign_SEEDBYTES) assert_not_equal(k1, k2) @@ -87,7 +91,7 @@ def test_different_keys_are_not_equal(self, k2): "seed", [b"77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a"], ) - def test_initialization_with_seed(self, seed): + def test_initialization_with_seed(self, seed: bytes): SigningKey(seed, encoder=HexEncoder) @pytest.mark.parametrize( @@ -95,7 +99,12 @@ def test_initialization_with_seed(self, seed): ed25519_known_answers(), ) def test_message_signing( - self, seed, _public_key, message, signature, expected + self, + seed: bytes, + _public_key: bytes, + message: bytes, + signature: bytes, + expected: bytes, ): signing_key = SigningKey( seed, @@ -140,7 +149,7 @@ def test_equal_keys_have_equal_hashes(self): VerifyKey(b"\x00" * (crypto_sign_PUBLICKEYBYTES - 1) + b"\x01"), ], ) - def test_different_keys_are_not_equal(self, k2): + def test_different_keys_are_not_equal(self, k2: Union[bytes, VerifyKey]): k1 = VerifyKey(b"\x00" * crypto_sign_PUBLICKEYBYTES) assert_not_equal(k1, k2) @@ -149,7 +158,12 @@ def test_different_keys_are_not_equal(self, k2): ed25519_known_answers(), ) def test_valid_signed_message( - self, _seed, public_key, message, signature, signed + self, + _seed: bytes, + public_key: bytes, + message: bytes, + signature: bytes, + signed: bytes, ): key = VerifyKey( public_key, @@ -260,12 +274,6 @@ def test_key_conversion(self): ) -def check_type_error(expected, f, *args): - with pytest.raises(TypeError) as e: - f(*args) - assert expected in str(e.value) - - def test_wrong_types(): sk = SigningKey.generate() @@ -287,7 +295,7 @@ def test_wrong_types(): "VerifyKey must be created from 32 bytes", VerifyKey, sk.verify_key ) - def verify_detached_signature(x): + def verify_detached_signature(x: bytes) -> None: sk.verify_key.verify(b"", x) check_type_error( diff --git a/tests/utils.py b/tests/utils.py index f1a40d2e..51aefeb2 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -14,19 +14,24 @@ import os +from typing import Callable, Dict, List, Optional, Tuple +import pytest -def assert_equal(x, y): + +def assert_equal(x: object, y: object) -> None: assert x == y assert not (x != y) -def assert_not_equal(x, y): +def assert_not_equal(x: object, y: object) -> None: assert x != y assert not (x == y) -def read_crypto_test_vectors(fname, maxels=0, delimiter=None): +def read_crypto_test_vectors( + fname: str, maxels: int = 0, delimiter: Optional[bytes] = None +) -> List[Tuple[bytes, ...]]: assert delimiter is not None and isinstance(delimiter, bytes) vectors = [] path = os.path.join(os.path.dirname(__file__), "data", fname) @@ -41,12 +46,16 @@ def read_crypto_test_vectors(fname, maxels=0, delimiter=None): return vectors -def read_kv_test_vectors(fname, delimiter=None, newrecord=None): +def read_kv_test_vectors( + fname: str, + delimiter: Optional[bytes] = None, + newrecord: Optional[bytes] = None, +) -> List[Dict[str, bytes]]: assert delimiter is not None and isinstance(delimiter, bytes) assert newrecord is not None and isinstance(newrecord, bytes) vectors = [] path = os.path.join(os.path.dirname(__file__), "data", fname) - vector = {} + vector: Dict[str, bytes] = {} with open(path, "rb") as fp: for line in fp: line = line.rstrip() @@ -61,9 +70,19 @@ def read_kv_test_vectors(fname, delimiter=None, newrecord=None): return vectors -def flip_byte(original, byte_offset): +def flip_byte(original: bytes, byte_offset: int) -> bytes: return ( original[:byte_offset] + bytes([0x01 ^ original[byte_offset]]) + original[byte_offset + 1 :] ) + + +# Type safety: it's fine to use `...` here, but mypy config doesn't like it because it's +# an explict `Any`. +def check_type_error( # type: ignore[misc] + expected: str, f: Callable[..., object], *args: object +) -> None: + with pytest.raises(TypeError) as e: + f(*args) + assert expected in str(e.value)