diff --git a/amazon/ion/util.py b/amazon/ion/util.py index 4d54338ae..6ce61c750 100644 --- a/amazon/ion/util.py +++ b/amazon/ion/util.py @@ -17,10 +17,12 @@ import sys from collections import namedtuple +from warnings import warn class _RecordMetaClass(type): """Metaclass for defining named-tuple based immutable record types.""" + def __new__(cls, name, bases, attrs): if attrs.get('_record_sentinel') is None: field_declarations = [] @@ -78,6 +80,7 @@ class MyRecord(record('a', ('b', 1))): Args: fields (list[str | (str, any)]): A sequence of str or pairs that """ + class RecordType(object, metaclass=_RecordMetaClass): _record_sentinel = True _record_fields = fields @@ -94,12 +97,14 @@ def coroutine(func): Returns: Callable: The decorated generator. """ + def wrapper(*args, **kwargs): gen = func(*args, **kwargs) val = next(gen) if val != None: raise TypeError('Unexpected value from start of coroutine') return gen + wrapper.__name__ = func.__name__ wrapper.__doc__ = func.__doc__ return wrapper @@ -142,6 +147,7 @@ class CodePoint(int): """Evaluates as the ordinal of a code point, while also containing the unicode character representation and indicating whether the code point was escaped. """ + def __init__(self, *args, **kwargs): self.char = None self.is_escaped = False @@ -193,6 +199,7 @@ def combine_surrogates(): real_code_point += (code_point - _HIGH_SURROGATE_START) << 10 real_code_point += (low_code_point - _LOW_SURROGATE_START) return real_code_point, low_surrogate + try: code_point, low = combine_surrogates() except StopIteration: @@ -217,15 +224,110 @@ def bit_length(value): return 0 return len(bin(abs(value))) - 2 + def total_seconds(td): return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10 ** 6) / 10 ** 6 else: def bit_length(value): return value.bit_length() + def total_seconds(td): return td.total_seconds() - bit_length.__doc__ = 'Returns the bit length of an integer' total_seconds.__doc__ = 'Timedelta ``total_seconds`` with backported support in Python 2.6' + + +class _EnumMetaClass(type): + """This is a deprecated, internal-only class in ion-python; do NOT use it for any reason. + + Metaclass for simple enumerations. + + Specifically provides the machinery necessary to emulate simplified Python 3.4 enumerations. + """ + + def __init__(cls, name, bases, attrs): + members = {} + # Re-bind any non magic-named method with an instance of the enumeration. + for attr_name, attr_value in iter(attrs.items()): + if not attr_name.startswith('_') and not callable(attr_value) and not isinstance(attr_value, property): + if not isinstance(attr_value, int): + raise TypeError('Enum value must be an int: %r' % attr_value) + actual_value = cls(attr_name, attr_value) + setattr(cls, attr_name, actual_value) + members[attr_value] = actual_value + + # Store the members reverse index. + cls._enum_members = members + + type.__init__(cls, name, bases, attrs) + + def __getitem__(cls, name): + """Looks up an enumeration value field by integer value.""" + return cls._enum_members[name] + + def __iter__(self): + """Iterates through the values of the enumeration in no specific order.""" + return iter(self._enum_members.values()) + + +class Enum(int, metaclass=_EnumMetaClass): + """This is a deprecated, internal-only class in ion-python; do NOT use it for any reason + + Simple integer based enumeration type. + + Examples: + The typical declaration looks like:: + + class MyEnum(Enum): + A = 1 + B = 2 + C = 3 + + At this point ``MyEnum.A`` is an instance of ``MyEnum``. + + Note: + Proper enumerations were added in Python 3.4 (PEP 435), this is a very simplified implementation + based loosely on that specification. + + In particular, implicit order of the values is not supported. + + Args: + value (int): the value associated with the enumeration. + + Attributes: + name (str): The name of the enum. + value (int): The original value associated with the enum. + """ + _enum_members = {} + + def __new__(cls, name, value): + return int.__new__(cls, value) + + def __init__(self, name, value): + warn(f'{self.__class__.__name__} is an internal-only class in ion-python; do not use it for any reason. This ' + f'class is deprecated and may be removed without further warning in any future release. Use `IntEnum` ' + f'instead.', + DeprecationWarning, stacklevel=2) + super().__init__() + self.name = name + self.value = value + + def __init_subclass__(cls, **kwargs): + warn(f'{cls.__name__} is an internal-only class in ion-python; do not use it for any reason. This ' + f'class is deprecated and may be removed without further warning in any future release. Use `IntEnum` ' + f'instead.', + DeprecationWarning, stacklevel=2) + super().__init_subclass__(**kwargs) + + def __getnewargs__(self): + return self.name, self.value + + def __str__(self): + return '<%s.%s: %s>' % (type(self).__name__, self.name, self.value) + + __repr__ = __str__ + + + diff --git a/ion-c b/ion-c index 093cc1c32..6c8cc1a44 160000 --- a/ion-c +++ b/ion-c @@ -1 +1 @@ -Subproject commit 093cc1c32d9ac3ba4b5f203b7538a9a8189bc9a2 +Subproject commit 6c8cc1a4436d5ac4c1aa876e24abb811b5fd5825 diff --git a/tests/test_util_enum.py b/tests/test_util_enum.py new file mode 100644 index 000000000..807f5caf3 --- /dev/null +++ b/tests/test_util_enum.py @@ -0,0 +1,54 @@ +# Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at: +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, 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 pytest + +from amazon.ion.util import Enum + + +class SimpleEnum(Enum): + A = 1 + B = 2 + + +def test_enum_members(): + assert SimpleEnum._enum_members == {1: SimpleEnum.A, 2: SimpleEnum.B} + + +def test_enum_reverse_lookup(): + assert SimpleEnum[1] == SimpleEnum.A + assert SimpleEnum[2] == SimpleEnum.B + + +def test_enum_fields(): + assert SimpleEnum.A.value == 1 + assert SimpleEnum.A.name == 'A' + assert SimpleEnum.B.value == 2 + assert SimpleEnum.B.name == 'B' + + values = list(SimpleEnum) + values.sort() + assert values == [SimpleEnum.A, SimpleEnum.B] + + +def test_enum_as_int(): + assert isinstance(SimpleEnum.A, int) + assert SimpleEnum.A == 1 + assert SimpleEnum.A is not 1 + + +def test_malformed_enum(): + with pytest.raises(TypeError): + class BadEnum(Enum): + A = 'Allo' \ No newline at end of file