bitmask.py: rewrite exceptions to be more human readable
This commit is contained in:
parent
456bc7b618
commit
0c333eee95
@ -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)
|
||||||
|
|
||||||
|
@ -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__
|
||||||
|
@ -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():
|
||||||
|
Loading…
Reference in New Issue
Block a user