Skip to content

Commit

Permalink
🎉 Finally fix integer type quirks!
Browse files Browse the repository at this point in the history
  • Loading branch information
puddly committed May 16, 2020
1 parent 87dd436 commit 72402da
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 51 deletions.
21 changes: 21 additions & 0 deletions tests/test_types_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@ class TE(t.enum_flag_uint16):
assert TE(0x8012).serialize() == data


def test_abstract_ints():
assert issubclass(t.uint8_t, t.uint_t)
assert not issubclass(t.uint8_t, t.int_t)
assert t.int_t._signed is True
assert t.uint_t._signed is False

with pytest.raises(TypeError):
t.int_t(0)

with pytest.raises(TypeError):
t.FixedIntType(0)


def test_int_too_short():
with pytest.raises(ValueError):
t.uint8_t.deserialize(b"")
Expand Down Expand Up @@ -159,3 +172,11 @@ class TestList(t.FixedList, length=3, item_type=t.uint16_t):
assert r[0] == 0x1234
assert r[1] == 0xAA55
assert r[2] == 0xAB89


def test_enum_instance_types():
class TestEnum(t.enum_uint8):
Member = 0x00

assert TestEnum._member_type_ is t.uint8_t
assert type(TestEnum.Member.value) is t.uint8_t
18 changes: 13 additions & 5 deletions tests/test_types_named.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,21 @@ def test_addr_mode_address():


def test_missing_status_enum():
assert 0x33 not in list(t.Status)
assert isinstance(t.Status(0x33), t.Status)
assert t.Status(0x33).value == 0x33
class TestEnum(t.MissingEnumMixin, t.enum_uint8):
Member = 0x00

# Status values that don't fit can't be created
assert 0xFF not in list(TestEnum)
assert isinstance(TestEnum(0xFF), TestEnum)
assert TestEnum(0xFF).value == 0xFF
assert type(TestEnum(0xFF).value) is t.uint8_t

# Missing members that don't fit can't be created
with pytest.raises(ValueError):
TestEnum(0xFF + 1)

# Missing members that aren't integers can't be created
with pytest.raises(ValueError):
t.Status(0xFF + 1)
TestEnum("0xFF")


def test_zdo_nullable_node_descriptor():
Expand Down
100 changes: 59 additions & 41 deletions zigpy_znp/types/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,32 @@ def serialize_list(objects) -> Bytes:
return Bytes(b"".join([o.serialize() for o in objects]))


class int_t(int):
_signed = True
class FixedIntType(int):
_signed = None
_size = None

def _concrete_new(cls, value=0):
instance = super().__new__(cls, value)
instance.serialize()

return instance

def __new__(cls, value):
instance = int.__new__(cls, value)
raise TypeError(f"Instances of abstract type {cls} cannot be created")

if instance._signed is not None and instance._size is not None:
# It's a concrete int_t type, check to make sure it's valid
instance.serialize()
def __init_subclass__(cls, signed=None, size=None, **kwargs) -> None:
if signed is not None:
cls._signed = signed

return instance
if size is not None:
cls._size = size

# XXX: The enum module uses the first class with `__new__` in its `__dict__`
# as the member type. We have to give each subclass its own `__new__`.
if signed is not None or size is not None:
cls.__new__ = cls._concrete_new

super().__init_subclass__(**kwargs)

def serialize(self) -> bytes:
try:
Expand All @@ -54,72 +68,76 @@ def deserialize(cls, data: bytes) -> typing.Tuple["int_t", bytes]:
return r, data


class int8s(int_t):
_size = 1
class uint_t(FixedIntType, signed=False):
pass


class int16s(int_t):
_size = 2
class int_t(FixedIntType, signed=True):
pass


class int8s(int_t, size=1):
pass


class int24s(int_t):
_size = 3
class int16s(int_t, size=2):
pass


class int32s(int_t):
_size = 4
class int24s(int_t, size=3):
pass


class int40s(int_t):
_size = 5
class int32s(int_t, size=4):
pass


class int48s(int_t):
_size = 6
class int40s(int_t, size=5):
pass


class int56s(int_t):
_size = 7
class int48s(int_t, size=6):
pass


class int64s(int_t):
_size = 8
class int56s(int_t, size=7):
pass


class uint_t(int_t):
_signed = False
class int64s(int_t, size=8):
pass


class uint8_t(uint_t):
_size = 1
class uint8_t(uint_t, size=1):
pass


class uint16_t(uint_t):
_size = 2
class uint16_t(uint_t, size=2):
pass


class uint24_t(uint_t):
_size = 3
class uint24_t(uint_t, size=3):
pass


class uint32_t(uint_t):
_size = 4
class uint32_t(uint_t, size=4):
pass


class uint40_t(uint_t):
_size = 5
class uint40_t(uint_t, size=5):
pass


class uint48_t(uint_t):
_size = 6
class uint48_t(uint_t, size=6):
pass


class uint56_t(uint_t):
_size = 7
class uint56_t(uint_t, size=7):
pass


class uint64_t(uint_t):
_size = 8
class uint64_t(uint_t, size=8):
pass


class ShortBytes(Bytes):
Expand Down
8 changes: 3 additions & 5 deletions zigpy_znp/types/named.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,14 +149,12 @@ class Schema:
class MissingEnumMixin:
@classmethod
def _missing_(cls, value):
if not isinstance(value, int) or value < 0 or value > 0xFF:
# `return None` works with Python 3.7.7, breaks with 3.7.1
if not isinstance(value, int):
raise ValueError(f"{value} is not a valid {cls.__name__}")

# XXX: infer type from enum
new_member = basic.uint8_t.__new__(cls, value)
new_member = cls._member_type_.__new__(cls, value)
new_member._name_ = f"unknown_0x{value:02X}"
new_member._value_ = value
new_member._value_ = cls._member_type_(value)

if sys.version_info >= (3, 8):
# Show the warning in the calling code, not in this function
Expand Down

0 comments on commit 72402da

Please sign in to comment.