From 4fe375b9a91578543fe85a9cc31fe4e90273c313 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Thu, 16 May 2019 14:16:42 -0400 Subject: [PATCH 1/5] Add FauxFloat, to round out numeric operations --- ppb/utils.py | 81 +++++++++++++++++++++++++++++++++++++++ tests/test_fauxfloat.py | 84 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 tests/test_fauxfloat.py diff --git a/ppb/utils.py b/ppb/utils.py index 13beb393..c8e6d477 100644 --- a/ppb/utils.py +++ b/ppb/utils.py @@ -1,5 +1,7 @@ import logging import sys +import numbers +import math __all__ = 'LoggingMixin', @@ -48,3 +50,82 @@ def logger(self): module_name = _get_module(file_name) return logging.getLogger(module_name) + + +class FauxFloat(numbers.Real): + """ + When applied to a class that implements __float__, provides the full suite + of number-related special methods. + + While this mixin doesn't do anything about it, you should consider makiing + your class immutable. Odd things could potentially happen otherwise. + """ + + def __abs__(self): + return float(self).__abs__() + + def __add__(self, other): + return float(self).__add__(other) + + def __ceil__(self): + return math.ceil(float(self)) + + def __eq__(self, other): + return float(self).__eq__(other) + + def __float__(self, other): + return float(self).__float__(other) + + def __floor__(self): + return math.floor(float(self)) + + def __floordiv__(self, other): + return float(self).__floordiv__(other) + + def __le__(self, other): + return float(self).__le__(other) + + def __lt__(self, other): + return float(self).__lt__(other) + + def __mod__(self, other): + return float(self).__mod__(other) + + def __mul__(self, other): + return float(self).__mul__(other) + + def __neg__(self): + return float(self).__neg__() + + def __pos__(self): + return float(self).__pos__() + + def __pow__(self, other): + return float(self).__pow__(other) + + def __radd__(self, other): + return float(self).__radd__(other) + + def __rfloordiv__(self, other): + return float(self).__rfloordiv__(other) + + def __rmod__(self, other): + return float(self).__rmod__(other) + + def __rmul__(self, other): + return float(self).__rmul__(other) + + def __round__(self, ndigits=None): + return float(self).__round__(ndigits) + + def __rpow__(self, other): + return float(self).__rpow__(other) + + def __rtruediv__(self, other): + return float(self).__rtruediv__(other) + + def __truediv__(self, other): + return float(self).__truediv__(other) + + def __trunc__(self): + return float(self).__trunc__() diff --git a/tests/test_fauxfloat.py b/tests/test_fauxfloat.py new file mode 100644 index 00000000..3b383b9e --- /dev/null +++ b/tests/test_fauxfloat.py @@ -0,0 +1,84 @@ +import math +import operator + +from ppb.utils import FauxFloat + +import pytest + +from hypothesis import given, assume +import hypothesis.strategies as st + + +def get_thingy(num): + class Thingy(FauxFloat): + def __float__(self): + return num + + return Thingy() + + +@pytest.mark.parametrize( + "operation", + [ + float, abs, bool, math.ceil, math.floor, math.trunc, operator.neg, + operator.pos, + ], +) +@given(num=st.floats(allow_nan=False, allow_infinity=False)) +def test_unary_ops(operation, num): + t = get_thingy(num) + + assert operation(t) == operation(num) + + +@pytest.mark.parametrize( + "operation", + [ + operator.lt, operator.le, operator.eq, operator.ne, operator.ge, + operator.gt, operator.add, operator.mul, operator.sub, + ], +) +@given( + num=st.floats(allow_nan=False, allow_infinity=False), + other=st.floats(allow_nan=False, allow_infinity=False), +) +def test_binary_ops(operation, num, other): + t = get_thingy(num) + + assert operation(t, other) == operation(num, other) + assert operation(other, t) == operation(other, t) + + +@pytest.mark.parametrize( + "operation", + [ + operator.floordiv, operator.mod, operator.truediv, + ], +) +@given( + num=st.floats(allow_nan=False, allow_infinity=False), + other=st.floats(allow_nan=False, allow_infinity=False), +) +def test_binary_ops_nonzero(operation, num, other): + assume(num != 0) + assume(other != 0) + t = get_thingy(num) + + assert operation(t, other) == operation(num, other) + assert operation(other, t) == operation(other, t) + + +@given( + num=st.floats(allow_nan=False, allow_infinity=False, min_value=-1e20, max_value=1e20), + other=st.floats(allow_nan=False, allow_infinity=False, min_value=-1e20, max_value=1e20), +) +def test_pow(operation, num, other): + assume(num != 0) + assume(other != 0) + t = get_thingy(num) + + assert operator.pow(t, other) == operator.pow(num, other) + assert operator.pow(other, t) == operator.pow(other, t) + + +# round From 7b919b0c04f5e22ec14cbefc1dc1fe2250539ddd Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Thu, 16 May 2019 14:51:56 -0400 Subject: [PATCH 2/5] Round out the test suite --- ppb/utils.py | 8 +++++++- tests/test_fauxfloat.py | 28 +++++++++++++++++----------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/ppb/utils.py b/ppb/utils.py index c8e6d477..1ca3032e 100644 --- a/ppb/utils.py +++ b/ppb/utils.py @@ -3,7 +3,7 @@ import numbers import math -__all__ = 'LoggingMixin', +__all__ = 'LoggingMixin', 'FauxFloat', # Dictionary mapping file names -> module names @@ -82,6 +82,12 @@ def __floor__(self): def __floordiv__(self, other): return float(self).__floordiv__(other) + def __ge__(self, other): + return float(self).__ge__(other) + + def __gt__(self, other): + return float(self).__gt__(other) + def __le__(self, other): return float(self).__le__(other) diff --git a/tests/test_fauxfloat.py b/tests/test_fauxfloat.py index 3b383b9e..21e934e1 100644 --- a/tests/test_fauxfloat.py +++ b/tests/test_fauxfloat.py @@ -17,6 +17,9 @@ def __float__(self): return Thingy() +# The use of parametrize() over st.sampled_from() is deliberate. + + @pytest.mark.parametrize( "operation", [ @@ -46,7 +49,7 @@ def test_binary_ops(operation, num, other): t = get_thingy(num) assert operation(t, other) == operation(num, other) - assert operation(other, t) == operation(other, t) + assert operation(other, t) == operation(other, num) @pytest.mark.parametrize( @@ -65,20 +68,23 @@ def test_binary_ops_nonzero(operation, num, other): t = get_thingy(num) assert operation(t, other) == operation(num, other) - assert operation(other, t) == operation(other, t) + assert operation(other, t) == operation(other, num) @given( - num=st.floats(allow_nan=False, allow_infinity=False, min_value=-1e20, max_value=1e20), - other=st.floats(allow_nan=False, allow_infinity=False, min_value=-1e20, max_value=1e20), + base=st.floats(allow_nan=False, allow_infinity=False, min_value=-1e20, max_value=1e20), + exponent=st.floats(allow_nan=False, allow_infinity=False, min_value=-10, max_value=10), ) -def test_pow(operation, num, other): - assume(num != 0) - assume(other != 0) - t = get_thingy(num) +def test_pow(base, exponent): + assume(base != 0 and exponent != 0) - assert operator.pow(t, other) == operator.pow(num, other) - assert operator.pow(other, t) == operator.pow(other, t) + assert operator.pow(get_thingy(base), exponent) == operator.pow(base, exponent) + assert operator.pow(base, get_thingy(exponent)) == operator.pow(base, exponent) -# round +@given( + num=st.floats(allow_nan=False, allow_infinity=False), + digits=st.integers() | st.none(), +) +def test_round(num, digits): + assert round(get_thingy(num), digits) == round(num, digits) From 3135918d36636f770620e67865eede7e8b09e57b Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Thu, 16 May 2019 14:56:59 -0400 Subject: [PATCH 3/5] sprites: Make Side a FauxFloat --- ppb/sprites.py | 50 ++++++++++---------------------------------------- 1 file changed, 10 insertions(+), 40 deletions(-) diff --git a/ppb/sprites.py b/ppb/sprites.py index f9b0cf8e..5235e2f1 100644 --- a/ppb/sprites.py +++ b/ppb/sprites.py @@ -4,6 +4,7 @@ from ppb import Vector from ppb.events import EventMixin +from ppb.utils import FauxFloat import ppb_vector.vector2 @@ -17,7 +18,7 @@ side_attribute_error_message = error_message.format -class Side: +class Side(FauxFloat): sides = { LEFT: ('x', -1), RIGHT: ('x', 1), @@ -33,51 +34,20 @@ def __repr__(self): return f"Side({self.parent!r}, {self.side!r})" def __str__(self): - return str(self.value) - - def __add__(self, other): - return self.value + other - - def __radd__(self, other): - return other + self.value - - def __sub__(self, other): - return self.value - other - - def __rsub__(self, other): - return other - self.value - - def __eq__(self, other): - return self.value == other - - def __le__(self, other): - return self.value <= other - - def __ge__(self, other): - return self.value >= other - - def __ne__(self, other): - return self.value != other - - def __gt__(self, other): - return self.value > other - - def __lt__(self, other): - return self.value < other + return str(float(self)) def _lookup_side(self, side): dimension, sign = self.sides[side] return dimension, sign * self.parent._offset_value - @property - def value(self): + def __float__(self): dimension, offset = self._lookup_side(self.side) return self.parent.position[dimension] + offset @property def top(self): self._attribute_gate(TOP, [TOP, BOTTOM]) - return Vector(self.value, self.parent.top.value) + return Vector(float(self), float(self.parent.top)) @top.setter def top(self, value): @@ -87,7 +57,7 @@ def top(self, value): @property def bottom(self): self._attribute_gate(BOTTOM, [TOP, BOTTOM]) - return Vector(self.value, self.parent.bottom.value) + return Vector(float(self), float(self.parent.bottom)) @bottom.setter def bottom(self, value): @@ -97,7 +67,7 @@ def bottom(self, value): @property def left(self): self._attribute_gate(LEFT, [LEFT, RIGHT]) - return Vector(self.parent.left.value, self.value) + return Vector(float(self.parent.left), float(self)) @left.setter def left(self, value): @@ -107,7 +77,7 @@ def left(self, value): @property def right(self): self._attribute_gate(RIGHT, [LEFT, RIGHT]) - return Vector(self.parent.right.value, self.value) + return Vector(float(self.parent.right), float(self)) @right.setter def right(self, value): @@ -117,9 +87,9 @@ def right(self, value): @property def center(self): if self.side in (TOP, BOTTOM): - return Vector(self.parent.center.x, self.value) + return Vector(self.parent.center.x, float(self)) else: - return Vector(self.value, self.parent.center.y) + return Vector(float(self), self.parent.center.y) @center.setter def center(self, value): From fecc3df85cff921d4129ccc85a3da7524498682a Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Thu, 16 May 2019 15:00:33 -0400 Subject: [PATCH 4/5] FauxFloat: Add int to tests --- tests/test_fauxfloat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_fauxfloat.py b/tests/test_fauxfloat.py index 21e934e1..6247e60a 100644 --- a/tests/test_fauxfloat.py +++ b/tests/test_fauxfloat.py @@ -23,7 +23,7 @@ def __float__(self): @pytest.mark.parametrize( "operation", [ - float, abs, bool, math.ceil, math.floor, math.trunc, operator.neg, + float, abs, bool, int, math.ceil, math.floor, math.trunc, operator.neg, operator.pos, ], ) From ca71ffbf241207ca569168bf8a8ce52bb7a9adea Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Fri, 17 May 2019 23:31:10 -0400 Subject: [PATCH 5/5] Add hypothesis to test requirements --- requirements-tests.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements-tests.txt b/requirements-tests.txt index e079f8a6..9b9e5f99 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1 +1,2 @@ pytest +hypothesis