bitmask.py: rewrite exceptions to be more human readable

This commit is contained in:
dogeystamp 2022-07-31 21:15:02 -04:00
parent 456bc7b618
commit 0c333eee95
Signed by: dogeystamp
GPG Key ID: 7225FE3592EFFA38
3 changed files with 37 additions and 12 deletions

View File

@ -3,7 +3,7 @@
"""Utilities for manipulating bitmasks.""" """Utilities for manipulating bitmasks."""
import bitmask.util as util from bitmask.util import type_name, fullname
class Bitmask: class Bitmask:
"""Generic bitmask, which represents multiple Enum values. """Generic bitmask, which represents multiple Enum values.
@ -65,7 +65,7 @@ class Bitmask:
@value.setter @value.setter
def value(self, value): def value(self, value):
if not issubclass(type(value), int): if not issubclass(type(value), int):
raise TypeError(f"value must be an integer (got '{type(value)}')") raise TypeError(f"value must be an integer")
self._value = value self._value = value
def __contains__(self, item): def __contains__(self, item):
@ -79,7 +79,7 @@ class Bitmask:
elif issubclass(type(item), self.AllFlags): elif issubclass(type(item), self.AllFlags):
return bool(self.value & item) return bool(self.value & item)
else: else:
raise TypeError(f"item must be an {type(self)} or {self.AllFlags} (got '{type(item)}')") raise TypeError(f"item must be {type_name(self)} or {type_name(self.AllFlags)}")
def __iter__(self): def __iter__(self):
"""Return list of enabled flags.""" """Return list of enabled flags."""
@ -97,13 +97,13 @@ class Bitmask:
return '|'.join([flag.name for flag in self]) or "0" return '|'.join([flag.name for flag in self]) or "0"
def __repr__(self): def __repr__(self):
enum_name = util.fullname(self.AllFlags(0)) enum_name = fullname(self.AllFlags(0))
args = ', '.join( args = ', '.join(
[enum_name] + [enum_name] +
[f"{enum_name}.{flag.name}" for flag in self] [f"{enum_name}.{flag.name}" for flag in self]
) )
return f"{util.fullname(self)}({args})" return f"{fullname(self)}({args})"
def __eq__(self, other): def __eq__(self, other):
"""Check equality.""" """Check equality."""
@ -123,7 +123,7 @@ class Bitmask:
bitmask, given the initial value and the flag. bitmask, given the initial value and the flag.
""" """
if not issubclass(type(flag), self.AllFlags): if not issubclass(type(flag), self.AllFlags):
raise TypeError(f"can only add {self.AllFlags} (not '{type(flag)}') to {type(self)}") raise TypeError(f"can only apply {type_name(self.AllFlags)} to {type_name(self)}")
self.value = op(self.value, flag) self.value = op(self.value, flag)
def __mask_op(self, other, op): def __mask_op(self, other, op):
@ -142,7 +142,7 @@ class Bitmask:
elif issubclass(type(other), self.AllFlags): elif issubclass(type(other), self.AllFlags):
new_bitmask._flag_op(other, op) new_bitmask._flag_op(other, op)
else: else:
raise TypeError(f"can only apply {type(self)} or {self.AllFlags} (not '{type(other)}') to {type(self)}") raise TypeError(f"can only apply {type_name(self)} or {type_name(self.AllFlags)} to {type_name(self)}")
return new_bitmask return new_bitmask
@ -231,7 +231,7 @@ class Bitmask:
TypeError: `flag` is not a single Enum value. TypeError: `flag` is not a single Enum value.
""" """
if not issubclass(type(flag), self._AllFlags): if not issubclass(type(flag), self._AllFlags):
raise TypeError(f"can only discard {self.AllFlags} (not '{type(flag)}') from {type(self)}") raise TypeError(f"can only discard {type_name(self._AllFlags)} from {type_name(self)}")
return self._flag_op(flag, lambda a, b : a & ~b) return self._flag_op(flag, lambda a, b : a & ~b)

View File

@ -3,6 +3,8 @@
"""Miscellaneous utilities for formatting exceptions, etc.""" """Miscellaneous utilities for formatting exceptions, etc."""
import enum
def fullname(obj): def fullname(obj):
"""Get the full class name of an object, including module. """Get the full class name of an object, including module.
@ -10,3 +12,20 @@ def fullname(obj):
String with class name, which can be used in an `eval()`. String with class name, which can be used in an `eval()`.
""" """
return f"{obj.__class__.__qualname__}" return f"{obj.__class__.__qualname__}"
def type_name(obj):
"""Get the short type of an object.
Returns:
String with human-readable type of the object.
Example:
import bitmask.util as util
util.type_name(1)
>>> int
"""
if issubclass(type(obj), enum.EnumMeta):
return obj.__name__
else:
return obj.__class__.__name__

View File

@ -24,6 +24,8 @@ def test_eq():
assert Bitmask(Desc, Desc.SMALL) != Desc.SMALL assert Bitmask(Desc, Desc.SMALL) != Desc.SMALL
assert Bitmask(Desc, Desc.SMALL) != Desc.ROUND assert Bitmask(Desc, Desc.SMALL) != Desc.ROUND
assert Bitmask(Desc) != Desc.ROUND assert Bitmask(Desc) != Desc.ROUND
assert Bitmask(Desc) != "Hello World!"
assert Bitmask(Desc) != 0
def test_repr(): def test_repr():
"""Ensure evaluating __repr__ creates an identical object.""" """Ensure evaluating __repr__ creates an identical object."""
@ -45,6 +47,9 @@ def test_add():
mask.add(Desc.ROUND) mask.add(Desc.ROUND)
assert mask == Bitmask(Desc, Desc.ROUND) assert mask == Bitmask(Desc, Desc.ROUND)
with pytest.raises(TypeError, match="can only apply Desc to Bitmask"):
mask.add(1)
def test_add_operator(): def test_add_operator():
"""Test + operator.""" """Test + operator."""
# Individual flags # Individual flags
@ -154,7 +159,7 @@ def test_discard():
empty_mask = Bitmask(Desc) empty_mask = Bitmask(Desc)
empty_mask.discard(Desc.SMALL) empty_mask.discard(Desc.SMALL)
assert empty_mask == Bitmask(Desc) assert empty_mask == Bitmask(Desc)
with pytest.raises(TypeError): with pytest.raises(TypeError, match="can only discard Desc from Bitmask"):
empty_mask.discard(Bitmask(Desc, Desc.SMALL)) empty_mask.discard(Bitmask(Desc, Desc.SMALL))
def test_subtract(): def test_subtract():
@ -220,9 +225,9 @@ def test_value():
assert mask == Bitmask(Desc) assert mask == Bitmask(Desc)
mask.value = 1 mask.value = 1
assert mask == Bitmask(Desc, Desc.SMALL) assert mask == Bitmask(Desc, Desc.SMALL)
with pytest.raises(TypeError): with pytest.raises(TypeError, match="value must be an integer"):
mask.value = 1j mask.value = 1j
with pytest.raises(TypeError): with pytest.raises(TypeError, match="value must be an integer"):
mask.value = 2.5 mask.value = 2.5
def test_contains(): def test_contains():
@ -239,8 +244,9 @@ def test_contains():
# Bitmasks # Bitmasks
assert mask in mask assert mask in mask
assert Bitmask(Desc, Desc.SMALL, Desc.FUNKY) in mask assert Bitmask(Desc, Desc.SMALL, Desc.FUNKY) in mask
assert Bitmask(Desc, Desc.SMALL, Desc.FUNKY, Desc.ROUND) not in mask
assert Bitmask(Desc, Desc.FUNKY) in mask assert Bitmask(Desc, Desc.FUNKY) in mask
with pytest.raises(TypeError): with pytest.raises(TypeError, match="item must be Bitmask or Desc"):
x = 1 in mask x = 1 in mask
def test_iter(): def test_iter():