Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement full numerical methods on the Sides API #272

Merged
merged 5 commits into from
May 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)