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

Support running tests locally, fix various warts #116

Merged
merged 8 commits into from
Mar 15, 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
5 changes: 2 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ cache:

before_install:
- source ./.ci/install-python.sh
- export TRAVIS_OS_NAME PYTHON

install:
- pip install --upgrade-strategy eager -U pip wheel setuptools
Expand All @@ -43,6 +44,4 @@ install:
- pip list

script:
- pytest --hypothesis-profile ci
- '[[ "$PYTHON" =~ pypy-* ]] || mypy ppb_vector tests'
- python -m doctest README.md
- ./test.sh
2 changes: 1 addition & 1 deletion dev-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pytest~=3.8
hypothesis
mypy==0.641
perf
pytest~=3.8
2 changes: 1 addition & 1 deletion ppb_vector/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from ppb_vector.vector2 import Vector2
from ppb_vector.vector2 import Vector2 # noqa
27 changes: 11 additions & 16 deletions ppb_vector/vector2.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import typing
import collections
import functools
import dataclasses
import functools
import typing
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from math import acos, atan2, cos, degrees, hypot, isclose, radians, sin, copysign, sqrt
from collections.abc import Sequence, Mapping
from math import atan2, copysign, cos, degrees, hypot, isclose, radians, sin, sqrt

__all__ = 'Vector2',
__all__ = ('Vector2',)


# Vector or subclass
Expand All @@ -32,8 +31,6 @@ def _find_lowest_type(left: typing.Type, right: typing.Type) -> typing.Type:
# has the most.
lmro = set(left.__mro__)
rmro = set(right.__mro__)
lspecial = lmro - rmro
rspecial = rmro - lmro
if len(lmro) > len(rmro):
return left
elif len(rmro) > len(lmro):
Expand Down Expand Up @@ -74,8 +71,7 @@ def __init__(self, *args, **kwargs):
f"got {len(args) + len(kwargs)}")

if kwargs and frozenset(kwargs) != {'x', 'y'}:
raise TypeError("Expected keyword arguments x and y, got: " +
kwargs.keys().join(', '))
raise TypeError(f"Expected keyword arguments x and y, got: {kwargs.keys().join(', ')}")

if kwargs:
x, y = kwargs['x'], kwargs['y']
Expand Down Expand Up @@ -231,7 +227,7 @@ def __neg__(self: VectorOrSub) -> VectorOrSub:
def angle(self: VectorOrSub, other: VectorLike) -> float:
other = Vector2.convert(other)

rv = degrees( atan2(other.x, -other.y) - atan2(self.x, -self.y) )
rv = degrees(atan2(other.x, -other.y) - atan2(self.x, -self.y))
# This normalizes the value to (-180, +180], which is the opposite of
# what Python usually does but is normal for angles
if rv <= -180:
Expand All @@ -242,7 +238,8 @@ def angle(self: VectorOrSub, other: VectorLike) -> float:
return rv

def isclose(self: VectorOrSub, other: VectorLike, *,
abs_tol: Realish=1e-3, rel_tol: Realish=1e-06, rel_to: typing.Sequence[VectorLike]=[]) -> bool:
abs_tol: Realish = 1e-3, rel_tol: Realish = 1e-06,
rel_to: typing.Sequence[VectorLike] = ()) -> bool:
"""
Determine whether two vectors are close in value.

Expand Down Expand Up @@ -272,10 +269,7 @@ def isclose(self: VectorOrSub, other: VectorLike, *,
)

diff = (self - other).length
return (
diff <= rel_tol * rel_length or
diff <= float(abs_tol)
)
return (diff <= rel_tol * rel_length or diff <= float(abs_tol))

@staticmethod
def _trig(angle: Realish) -> typing.Tuple[float, float]:
Expand Down Expand Up @@ -335,4 +329,5 @@ def reflect(self: VectorOrSub, surface_normal: VectorLike) -> VectorOrSub:

return self - (2 * (self * surface_normal) * surface_normal)


Sequence.register(Vector2)
21 changes: 21 additions & 0 deletions test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail

function run() {
echo '$' "$@"
"$@"
echo
}

if [[ -v TRAVIS_OS_NAME ]]; then
IN_CI=1
PYTEST_OPTIONS=( --hypothesis-profile ci )
else
IN_CI=0
PYTEST_OPTIONS=( )
fi


run python -m doctest README.md
[[ "${PYTHON-x}" =~ pypy-* ]] || run mypy ppb_vector tests
run pytest "${PYTEST_OPTIONS[@]}"
9 changes: 5 additions & 4 deletions tests/benchmark.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
import perf # type: ignore
import perf # type: ignore

from ppb_vector import Vector2
from utils import *

Expand All @@ -8,11 +9,11 @@
y = Vector2(0, 1)
scalar = 123

for f in BINARY_OPS + BINARY_SCALAR_OPS + BOOL_OPS: # type: ignore
for f in BINARY_OPS + BINARY_SCALAR_OPS + BOOL_OPS: # type: ignore
r.bench_func(f.__name__, f, x, y)

for f in UNARY_OPS + UNARY_SCALAR_OPS: # type: ignore
for f in UNARY_OPS + UNARY_SCALAR_OPS: # type: ignore
r.bench_func(f.__name__, f, x)

for f in SCALAR_OPS: # type: ignore
for f in SCALAR_OPS: # type: ignore
r.bench_func(f.__name__, f, x, scalar)
3 changes: 1 addition & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import os

def setup_hypothesis():
from hypothesis import settings, Verbosity

settings.register_profile("ci", max_examples=1000)
settings.register_profile("dev", max_examples=10)
settings.register_profile("debug", max_examples=10, verbosity=Verbosity.verbose)
Expand Down
17 changes: 12 additions & 5 deletions tests/test_convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@
from ppb_vector import Vector2
from utils import vector_likes, vectors

class V(Vector2): pass

@pytest.mark.parametrize('vector_like', vector_likes(), ids=lambda x: type(x).__name__) # type: ignore
@pytest.mark.parametrize('cls', [Vector2, V]) # type: ignore
class V(Vector2):
pass


@pytest.mark.parametrize(
"vector_like", vector_likes(), ids=lambda x: type(x).__name__,
)
@pytest.mark.parametrize("cls", [Vector2, V]) # type: ignore
def test_convert_class(cls, vector_like):
vector = cls.convert(vector_like)
assert isinstance(vector, cls)
Expand All @@ -18,22 +23,24 @@ def test_convert_class(cls, vector_like):
def test_convert_tuple(vector: Vector2):
assert vector == tuple(vector) == (vector.x, vector.y)


@given(vector=vectors())
def test_convert_list(vector: Vector2):
assert vector == list(vector) == [vector.x, vector.y]


@given(vector=vectors())
def test_convert_dict(vector: Vector2):
assert vector == vector.asdict()


@pytest.mark.parametrize('coerce', [tuple, list, Vector2.asdict])
@pytest.mark.parametrize("coerce", [tuple, list, Vector2.asdict])
@given(x=vectors())
def test_convert_roundtrip(coerce, x: Vector2):
assert x == Vector2(coerce(x))


@pytest.mark.parametrize('coerce', [tuple, list])
@pytest.mark.parametrize("coerce", [tuple, list])
@given(x=vectors())
def test_convert_roundtrip_positional(coerce, x: Vector2):
assert x == Vector2(*coerce(x))
17 changes: 11 additions & 6 deletions tests/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,38 +7,43 @@

class V1(Vector2):
"""Arbitrary subclass of Vector2."""

pass


class V11(V1):
"""Subclass of V1."""

pass


class V2(Vector2):
"""Arbitrary subclass of Vector2, distinct from V1."""

pass


@pytest.mark.parametrize('op', BINARY_OPS)
@pytest.mark.parametrize("op", BINARY_OPS)
@given(x=st.builds(V1, vectors()), y=st.builds(V1, units()))
def test_binop_same(op, x: V1, y: V2):
assert isinstance(op(x, y), V1)


@pytest.mark.parametrize('op', BINARY_OPS)
@pytest.mark.parametrize("op", BINARY_OPS)
@given(x=vectors(), y=units())
def test_binop_different(op, x: Vector2, y: Vector2):
assert isinstance(op(V1(x), V2(y)), (V1, V2))
assert isinstance(op(V2(x), V1(y)), (V1, V2))


@pytest.mark.parametrize('op', BINARY_OPS)
@pytest.mark.parametrize("op", BINARY_OPS)
@given(x=st.builds(V1, vectors()), y=st.builds(V1, units()))
def test_binop_subclass(op, x: V1, y: V1):
assert isinstance(op(V11(x), y), V11)
assert isinstance(op(x, V11(y)), V11)


@pytest.mark.parametrize('op', SCALAR_OPS)
@pytest.mark.parametrize("op", SCALAR_OPS)
@given(x=st.builds(V1, vectors()), scalar=floats())
def test_vnumop(op, x: V1, scalar: float):
try:
Expand All @@ -48,7 +53,7 @@ def test_vnumop(op, x: V1, scalar: float):
reject()


@pytest.mark.parametrize('op', UNARY_OPS)
@pytest.mark.parametrize("op", UNARY_OPS)
@given(x=st.builds(V1, vectors()))
def test_monop(op, x):
try:
Expand All @@ -58,7 +63,7 @@ def test_monop(op, x):
reject()


@pytest.mark.parametrize('op', BINARY_OPS + BINARY_SCALAR_OPS + BOOL_OPS) # type: ignore
@pytest.mark.parametrize("op", BINARY_OPS + BINARY_SCALAR_OPS + BOOL_OPS) # type: ignore
@given(x=vectors(), y=units())
def test_binop_vectorlike(op, x: Vector2, y: Vector2):
"""Test that `op` accepts a vector-like second parameter."""
Expand Down
7 changes: 4 additions & 3 deletions tests/test_vector2_addition.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytest # type: ignore

from ppb_vector import Vector2


Expand All @@ -25,15 +26,15 @@ def test_addition_vector_list():

def test_addition_vector_dict():
test_vector = Vector2(1, 1)
test_dict = {'x': 3, 'y': 5}
test_dict = {"x": 3, "y": 5}
result = test_vector + test_dict
assert result == Vector2(4, 6)


data = [
([Vector2(1, 1), (2, 2)], Vector2(3, 3)),
([Vector2(1, 2), [2, 2]], Vector2(3, 4)),
([Vector2(1, 2), {'x': 2, 'y': 2}], Vector2(3, 4)),
([Vector2(1, 2), {"x": 2, "y": 2}], Vector2(3, 4)),
([Vector2(10, 16), Vector2(2, 2)], Vector2(12, 18)),
([Vector2(25, 22), (12, 92)], Vector2(37, 114)),
([Vector2(25, 22), Vector2(22, 61)], Vector2(47, 83)),
Expand All @@ -43,6 +44,6 @@ def test_addition_vector_dict():
]


@pytest.mark.parametrize('test_input, expected', data)
@pytest.mark.parametrize("test_input, expected", data)
def test_multiples_values(test_input, expected):
assert (test_input[0] + test_input[1]) == expected
40 changes: 20 additions & 20 deletions tests/test_vector2_angle.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
from ppb_vector import Vector2
from math import isclose

import pytest # type: ignore
from hypothesis import assume, given, note
from hypothesis import assume, given

from ppb_vector import Vector2
from utils import angle_isclose, floats, vectors


@pytest.mark.parametrize("left, right, expected", [
(Vector2(1, 1), Vector2(0, -1), -135),
(Vector2(1, 1), Vector2(-1, 0), 135),
(Vector2(0, 1), Vector2(0, -1), 180),
(Vector2(-1, -1), Vector2(1, 0), 135),
(Vector2(-1, -1), Vector2(-1, 0), -45),
(Vector2(1, 0), Vector2(0, 1), 90),
(Vector2(1, 0), Vector2(1, 0), 0),
])
@pytest.mark.parametrize(
"left, right, expected",
[
(Vector2(1, 1), Vector2(0, -1), -135),
(Vector2(1, 1), Vector2(-1, 0), 135),
(Vector2(0, 1), Vector2(0, -1), 180),
(Vector2(-1, -1), Vector2(1, 0), 135),
(Vector2(-1, -1), Vector2(-1, 0), -45),
(Vector2(1, 0), Vector2(0, 1), 90),
(Vector2(1, 0), Vector2(1, 0), 0),
],
)
def test_angle(left, right, expected):
lr = left.angle(right)
rl = right.angle(left)
Expand All @@ -23,10 +28,7 @@ def test_angle(left, right, expected):
assert isclose(rl, 180 if expected == 180 else -expected)


@given(
left=vectors(),
right=vectors(),
)
@given(left=vectors(), right=vectors())
def test_angle_range(left, right):
"""Vector2.angle produces values in [-180; 180] and is antisymmetric.

Expand All @@ -38,18 +40,16 @@ def test_angle_range(left, right):
assert -180 < rl <= 180
assert angle_isclose(lr, -rl)

@given(
left=vectors(),
middle=vectors(),
right=vectors(),
)

@given(left=vectors(), middle=vectors(), right=vectors())
def test_angle_additive(left, middle, right):
"""left.angle(middle) + middle.angle(right) == left.angle(right)"""
lm = left.angle(middle)
mr = middle.angle(right)
lr = left.angle(right)
assert angle_isclose(lm + mr, lr)


@given(x=vectors(), scalar=floats())
def test_angle_aligned(x: Vector2, scalar: float):
"""x.angle(scalar * x) is 0 or 180, depending on whether scalar > 0"""
Expand Down
9 changes: 5 additions & 4 deletions tests/test_vector2_ctor.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import pytest # type: ignore
from hypothesis import given
from utils import floats, vectors, vector_likes

from ppb_vector import Vector2
from utils import floats, vector_likes, vectors


class V(Vector2): pass
class V(Vector2):
pass


@pytest.mark.parametrize('cls', [Vector2, V])
@pytest.mark.parametrize("cls", [Vector2, V])
@given(x=vectors())
def test_ctor_vector_like(cls, x: Vector2):
for x_like in vector_likes(x):
Expand All @@ -17,7 +18,7 @@ def test_ctor_vector_like(cls, x: Vector2):
assert isinstance(vector, cls)


@pytest.mark.parametrize('cls', [Vector2, V])
@pytest.mark.parametrize("cls", [Vector2, V])
@given(x=floats(), y=floats())
def test_ctor_coordinates(cls, x: float, y: float):
assert cls(x, y) == cls((x, y))
Loading