From a108db57a15e96417239967ffbbe1b0af4942ddf Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sun, 15 Oct 2017 13:04:16 +1300 Subject: [PATCH] [#62] Renamed trytes codec to trytes_ascii. The previous name will still work, but it is deprecated and will be removed in PyOTA v2.1. --- iota/__init__.py | 2 +- iota/codecs.py | 46 +++++++++++++++++++--- iota/transaction/base.py | 2 +- iota/types.py | 65 +++++++++++++++++++------------ test/codecs_test.py | 81 +++++++++++++++++++++++++++------------ test/crypto/types_test.py | 7 +++- test/types_test.py | 6 +-- 7 files changed, 148 insertions(+), 61 deletions(-) diff --git a/iota/__init__.py b/iota/__init__.py index 9f28d4c9..498a2298 100644 --- a/iota/__init__.py +++ b/iota/__init__.py @@ -28,7 +28,7 @@ } -# Activate TrytesCodec. +# Activate codecs. from .codecs import * # Make some imports accessible from the top level of the package. diff --git a/iota/codecs.py b/iota/codecs.py index 594498c8..640eb084 100644 --- a/iota/codecs.py +++ b/iota/codecs.py @@ -3,12 +3,13 @@ unicode_literals from codecs import Codec, CodecInfo, register as lookup_function +from warnings import warn from iota.exceptions import with_context from six import PY3, binary_type __all__ = [ - 'TrytesCodec', + 'AsciiTrytesCodec', 'TrytesDecodeError', ] @@ -20,11 +21,26 @@ class TrytesDecodeError(ValueError): pass -class TrytesCodec(Codec): +class AsciiTrytesCodec(Codec): """ - Codec for converting byte strings into trytes, and vice versa. + Legacy codec for converting byte strings into trytes, and vice versa. + + This method encodes each pair of trytes as an ASCII code point (and + vice versa when decoding). + + The end result requires more space than if the trytes were converted + mathematically, but because the result is ASCII, it's easier to work + with. + + Think of this kind of like Base 64 for balanced ternary (: + """ + name = 'trytes_ascii' + + compat_name = 'trytes' + """ + Old name for this codec. + Note: Will be removed in PyOTA v2.1! """ - name = 'trytes' # :bc: Without the bytearray cast, Python 2 will populate the dict # with characters instead of integers. @@ -173,7 +189,25 @@ def decode(self, input, errors='strict'): @lookup_function def check_trytes_codec(encoding): - if encoding == TrytesCodec.name: - return TrytesCodec.get_codec_info() + """ + Determines which codec to use for the specified encoding. + + References: + - https://docs.python.org/3/library/codecs.html#codecs.register + """ + if encoding == AsciiTrytesCodec.name: + return AsciiTrytesCodec.get_codec_info() + + elif encoding == AsciiTrytesCodec.compat_name: + warn( + '"{old_codec}" codec will be removed in PyOTA v2.1. ' + 'Use "{new_codec}" instead.'.format( + new_codec = AsciiTrytesCodec.name, + old_codec = AsciiTrytesCodec.compat_name, + ), + + DeprecationWarning, + ) + return AsciiTrytesCodec.get_codec_info() return None diff --git a/iota/transaction/base.py b/iota/transaction/base.py index 524e66ca..de76c623 100644 --- a/iota/transaction/base.py +++ b/iota/transaction/base.py @@ -457,7 +457,7 @@ def tail_transaction(self): return self[0] def get_messages(self, errors='drop'): - # type: () -> List[Text] + # type: (Text) -> List[Text] """ Attempts to decipher encoded messages from the transactions in the bundle. diff --git a/iota/types.py b/iota/types.py index f4db6aa6..47544302 100644 --- a/iota/types.py +++ b/iota/types.py @@ -6,13 +6,13 @@ from itertools import chain from math import ceil from random import SystemRandom -from typing import AnyStr, Generator, Iterable, Iterator, List, \ +from typing import Any, AnyStr, Generator, Iterable, Iterator, List, \ MutableSequence, Optional, Text, Union from six import PY2, binary_type, itervalues, python_2_unicode_compatible, \ text_type -from iota import TRITS_PER_TRYTE, TrytesCodec +from iota import AsciiTrytesCodec, TRITS_PER_TRYTE from iota.crypto import HASH_LENGTH from iota.crypto.kerl import Kerl from iota.exceptions import with_context @@ -99,7 +99,7 @@ def random(cls, length): :param length: Number of trytes to generate. """ - alphabet = list(itervalues(TrytesCodec.alphabet)) + alphabet = list(itervalues(AsciiTrytesCodec.alphabet)) generator = SystemRandom() # :py:meth:`SystemRandom.choices` wasn't added until Python 3.6; @@ -111,30 +111,35 @@ def random(cls, length): ) @classmethod - def from_bytes(cls, bytes_, *args, **kwargs): - # type: (Union[binary_type, bytearray], ...) -> TryteString + def from_bytes(cls, bytes_, codec=AsciiTrytesCodec.name, *args, **kwargs): + # type: (Union[binary_type, bytearray], Text, *Any, **Any) -> TryteString """ Creates a TryteString from a sequence of bytes. :param bytes_: Source bytes. + :param codec: + Which codec to use: + + - 'binary': Converts each byte into a sequence of trits with + the same value (this is usually what you want). + - 'ascii': Uses the legacy ASCII codec. + :param args: Additional positional arguments to pass to the initializer. :param kwargs: Additional keyword arguments to pass to the initializer. """ - return cls(encode(bytes_, 'trytes'), *args, **kwargs) + return cls(encode(bytes_, codec), *args, **kwargs) @classmethod def from_string(cls, string, *args, **kwargs): - # type: (Text, ...) -> TryteString + # type: (Text, *Any, **Any) -> TryteString """ Creates a TryteString from a Unicode string. - Note: The string will be encoded using UTF-8. - :param string: Source string. @@ -144,11 +149,16 @@ def from_string(cls, string, *args, **kwargs): :param kwargs: Additional keyword arguments to pass to the initializer. """ - return cls.from_bytes(string.encode('utf-8'), *args, **kwargs) + return cls.from_bytes( + bytes_ = string.encode('utf-8'), + codec = AsciiTrytesCodec.name, + *args, + **kwargs + ) @classmethod def from_trytes(cls, trytes, *args, **kwargs): - # type: (Iterable[Iterable[int]], ...) -> TryteString + # type: (Iterable[Iterable[int]], *Any, **Any) -> TryteString """ Creates a TryteString from a sequence of trytes. @@ -174,13 +184,13 @@ def from_trytes(cls, trytes, *args, **kwargs): if converted < 0: converted += 27 - chars.append(TrytesCodec.alphabet[converted]) + chars.append(AsciiTrytesCodec.alphabet[converted]) return cls(chars, *args, **kwargs) @classmethod def from_trits(cls, trits, *args, **kwargs): - # type: (Iterable[int], ...) -> TryteString + # type: (Iterable[int], *Any, **Any) -> TryteString """ Creates a TryteString from a sequence of trits. @@ -275,7 +285,7 @@ def __init__(self, trytes, pad=None): trytes = bytearray(trytes) for i, ordinal in enumerate(trytes): - if ordinal not in TrytesCodec.index: + if ordinal not in AsciiTrytesCodec.index: raise with_context( exc = ValueError( 'Invalid character {char!r} at position {i} ' @@ -313,9 +323,9 @@ def __bytes__(self): Note: This does not decode the trytes into bytes/characters; it only returns an ASCII representation of the trytes themselves! - If you want to: - - Decode trytes into bytes: use :py:meth:`as_bytes`. - - Decode trytes into Unicode: use :py:meth:`as_string`. + If you want to... + - ... decode trytes into bytes: use :py:meth:`as_bytes`. + - ... decode trytes into Unicode: use :py:meth:`as_string`. """ return binary_type(self._trytes) @@ -479,8 +489,8 @@ def iter_chunks(self, chunk_size): """ return ChunkIterator(self, chunk_size) - def as_bytes(self, errors='strict'): - # type: (Text) -> binary_type + def as_bytes(self, errors='strict', codec=AsciiTrytesCodec.name): + # type: (Text, Text) -> binary_type """ Converts the TryteString into a byte string. @@ -490,12 +500,19 @@ def as_bytes(self, errors='strict'): - 'replace': replace with '?'. - 'ignore': omit the tryte from the result. + :param codec: + Which codec to use: + + - 'binary': Converts each sequence of 5 trits into a byte with + the same value (this is usually what you want). + - 'ascii': Uses the legacy ASCII codec. + :raise: - :py:class:`iota.codecs.TrytesDecodeError` if the trytes cannot be decoded into bytes. """ - # :bc: In Python 2, `decode` does not accept keyword arguments. - return decode(self._trytes, 'trytes', errors) + # In Python 2, :py:func:`decode` does not accept keyword arguments. + return decode(self._trytes, codec, errors) def as_string(self, errors='strict', strip_padding=True): # type: (Text, bool) -> Text @@ -523,10 +540,10 @@ def as_string(self, errors='strict', strip_padding=True): if strip_padding and (trytes[-1] == ord(b'9')): trytes = trytes.rstrip(b'9') - # Put one back to preserve even length. + # Put one back to preserve even length for ASCII codec. trytes += b'9' * (len(trytes) % 2) - return decode(trytes, 'trytes', errors).decode('utf-8', errors) + return decode(trytes, AsciiTrytesCodec.name, errors).decode('utf-8', errors) def as_json_compatible(self): # type: () -> Text @@ -546,7 +563,7 @@ def as_integers(self): Each integer is a value between -13 and 13. """ return [ - self._normalize(TrytesCodec.index[c]) + self._normalize(AsciiTrytesCodec.index[c]) for c in self._trytes ] diff --git a/test/codecs_test.py b/test/codecs_test.py index 79c44ed4..32a5f300 100644 --- a/test/codecs_test.py +++ b/test/codecs_test.py @@ -2,53 +2,67 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from codecs import encode, decode +from codecs import decode, encode from unittest import TestCase +from warnings import catch_warnings, simplefilter as simple_filter +from six import text_type -# noinspection SpellCheckingInspection -from iota.codecs import TrytesDecodeError +from iota.codecs import AsciiTrytesCodec, TrytesDecodeError # noinspection SpellCheckingInspection -class TrytesCodecTestCase(TestCase): +class AsciiTrytesCodecTestCase(TestCase): def test_encode_byte_string(self): - """Encoding a byte string into trytes.""" + """ + Encoding a byte string into trytes. + """ self.assertEqual( - encode(b'Hello, IOTA!', 'trytes'), + encode(b'Hello, IOTA!', AsciiTrytesCodec.name), b'RBTC9D9DCDQAEASBYBCCKBFA', ) def test_encode_bytearray(self): - """Encoding a bytearray into trytes.""" + """ + Encoding a bytearray into trytes. + """ self.assertEqual( - encode(bytearray(b'Hello, IOTA!'), 'trytes'), + encode(bytearray(b'Hello, IOTA!'), AsciiTrytesCodec.name), b'RBTC9D9DCDQAEASBYBCCKBFA', ) def test_encode_error_wrong_type(self): - """Attempting to encode a value with an incompatible type.""" + """ + Attempting to encode a value with an incompatible type. + """ with self.assertRaises(TypeError): # List value not accepted; it can contain things other than bytes - # (ordinals in range(255), that is). - encode([72, 101, 108, 108, 111, 44, 32, 73, 79, 84, 65, 33], 'trytes') + # (ordinals in range(255), that is). + encode( + [72, 101, 108, 108, 111, 44, 32, 73, 79, 84, 65, 33], + AsciiTrytesCodec.name, + ) with self.assertRaises(TypeError): # Unicode strings not accepted; it is ambiguous whether and how # to encode to bytes. - encode('Hello, IOTA!', 'trytes') + encode('Hello, IOTA!', AsciiTrytesCodec.name) def test_decode_byte_string(self): - """Decoding trytes to a byte string.""" + """ + Decoding trytes to a byte string. + """ self.assertEqual( - decode(b'RBTC9D9DCDQAEASBYBCCKBFA', 'trytes'), + decode(b'RBTC9D9DCDQAEASBYBCCKBFA', AsciiTrytesCodec.name), b'Hello, IOTA!', ) def test_decode_bytearray(self): - """Decoding a bytearray of trytes into a byte string.""" + """ + Decoding a bytearray of trytes into a byte string. + """ self.assertEqual( - decode(bytearray(b'RBTC9D9DCDQAEASBYBCCKBFA'), 'trytes'), + decode(bytearray(b'RBTC9D9DCDQAEASBYBCCKBFA'), AsciiTrytesCodec.name), b'Hello, IOTA!', ) @@ -57,14 +71,14 @@ def test_decode_wrong_length_errors_strict(self): Attempting to decode an odd number of trytes with errors='strict'. """ with self.assertRaises(TrytesDecodeError): - decode(b'RBTC9D9DCDQAEASBYBCCKBFA9', 'trytes', 'strict') + decode(b'RBTC9D9DCDQAEASBYBCCKBFA9', AsciiTrytesCodec.name, 'strict') def test_decode_wrong_length_errors_ignore(self): """ Attempting to decode an odd number of trytes with errors='ignore'. """ self.assertEqual( - decode(b'RBTC9D9DCDQAEASBYBCCKBFA9', 'trytes', 'ignore'), + decode(b'RBTC9D9DCDQAEASBYBCCKBFA9', AsciiTrytesCodec.name, 'ignore'), b'Hello, IOTA!', ) @@ -73,34 +87,51 @@ def test_decode_wrong_length_errors_replace(self): Attempting to decode an odd number of trytes with errors='replace'. """ self.assertEqual( - decode(b'RBTC9D9DCDQAEASBYBCCKBFA9', 'trytes', 'replace'), + decode(b'RBTC9D9DCDQAEASBYBCCKBFA9', AsciiTrytesCodec.name, 'replace'), b'Hello, IOTA!?', ) def test_decode_invalid_pair_errors_strict(self): """ Attempting to decode an un-decodable pair of trytes with - errors='strict'. + errors='strict'. """ with self.assertRaises(TrytesDecodeError): - decode(b'ZJVYUGTDRPDYFGFXMK', 'trytes', 'strict') + decode(b'ZJVYUGTDRPDYFGFXMK', AsciiTrytesCodec.name, 'strict') def test_decode_invalid_pair_errors_ignore(self): """ Attempting to decode an un-decodable pair of trytes with - errors='ignore'. + errors='ignore'. """ self.assertEqual( - decode(b'ZJVYUGTDRPDYFGFXMK', 'trytes', 'ignore'), + decode(b'ZJVYUGTDRPDYFGFXMK', AsciiTrytesCodec.name, 'ignore'), b'\xd2\x80\xc3', ) def test_decode_invalid_pair_errors_replace(self): """ Attempting to decode an un-decodable pair of trytes with - errors='replace'. + errors='replace'. """ self.assertEqual( - decode(b'ZJVYUGTDRPDYFGFXMK', 'trytes', 'replace'), + decode(b'ZJVYUGTDRPDYFGFXMK', AsciiTrytesCodec.name, 'replace'), b'??\xd2\x80??\xc3??', ) + + def test_compat_name(self): + """ + A warning is raised when using the codec's old name. + """ + with catch_warnings(record=True) as warnings: + simple_filter('always', category=DeprecationWarning) + + self.assertEqual( + # Provide the old codec name to :py:func:`encode`. + encode(b'Hello, IOTA!', AsciiTrytesCodec.compat_name), + b'RBTC9D9DCDQAEASBYBCCKBFA', + ) + + self.assertEqual(len(warnings), 1) + self.assertEqual(warnings[0].category, DeprecationWarning) + self.assertIn('codec will be removed', text_type(warnings[0].message)) diff --git a/test/crypto/types_test.py b/test/crypto/types_test.py index a2664e64..79358d8e 100644 --- a/test/crypto/types_test.py +++ b/test/crypto/types_test.py @@ -5,6 +5,8 @@ import warnings from unittest import TestCase +from six import text_type + from iota import Hash, TryteString from iota.crypto import SeedWarning from iota.crypto.types import Digest, PrivateKey, Seed @@ -48,7 +50,10 @@ def test_random_seed_too_long(self): # check attributes of warning self.assertEqual(len(catched_warnings), 1) self.assertIs(catched_warnings[-1].category, SeedWarning) - self.assertIn("inappropriate length", str(catched_warnings[-1].message)) + self.assertIn( + "inappropriate length", + text_type(catched_warnings[-1].message), + ) self.assertEqual(len(seed), Hash.LEN + 1) diff --git a/test/types_test.py b/test/types_test.py index 69429ffc..c1a95d84 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -7,7 +7,7 @@ from six import binary_type, text_type from iota import Address, AddressChecksum, Hash, Tag, TryteString, \ - TrytesCodec, TrytesDecodeError, trits_from_int + AsciiTrytesCodec, TrytesDecodeError, trits_from_int class TritsFromIntTestCase(TestCase): @@ -592,7 +592,7 @@ def test_as_trytes_single_tryte(self): self.assertDictEqual( { chr(c): TryteString(chr(c).encode('ascii')).as_trytes() - for c in TrytesCodec.alphabet.values() + for c in AsciiTrytesCodec.alphabet.values() }, { @@ -666,7 +666,7 @@ def test_as_trits_single_tryte(self): self.assertDictEqual( { chr(c): TryteString(chr(c).encode('ascii')).as_trits() - for c in TrytesCodec.alphabet.values() + for c in AsciiTrytesCodec.alphabet.values() }, {