Skip to content

Commit

Permalink
Merge #272
Browse files Browse the repository at this point in the history
272: Implement full numerical methods on the Sides API r=pathunstrom a=astronouth7303

Closes #193 by making Side objects pretend to be numbers.

Co-authored-by: Jamie Bliss <jamie@ivyleav.es>
  • Loading branch information
bors[bot] and AstraLuma committed May 19, 2019
2 parents 929f58a + ca71ffb commit 204791c
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 41 deletions.
50 changes: 10 additions & 40 deletions ppb/sprites.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from ppb import Vector
from ppb.events import EventMixin
from ppb.utils import FauxFloat

import ppb_vector.vector2

Expand All @@ -17,7 +18,7 @@
side_attribute_error_message = error_message.format


class Side:
class Side(FauxFloat):
sides = {
LEFT: ('x', -1),
RIGHT: ('x', 1),
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand Down
89 changes: 88 additions & 1 deletion ppb/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import logging
import sys
import numbers
import math

__all__ = 'LoggingMixin',
__all__ = 'LoggingMixin', 'FauxFloat',


# Dictionary mapping file names -> module names
Expand Down Expand Up @@ -48,3 +50,88 @@ 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 __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)

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__()
1 change: 1 addition & 0 deletions requirements-tests.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pytest
hypothesis
90 changes: 90 additions & 0 deletions tests/test_fauxfloat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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()


# The use of parametrize() over st.sampled_from() is deliberate.


@pytest.mark.parametrize(
"operation",
[
float, abs, bool, int, 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, num)


@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, num)


@given(
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(base, exponent):
assume(base != 0 and exponent != 0)

assert operator.pow(get_thingy(base), exponent) == operator.pow(base, exponent)
assert operator.pow(base, get_thingy(exponent)) == operator.pow(base, exponent)


@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)

0 comments on commit 204791c

Please sign in to comment.