From 9814f3565690f11d0d40c5470c1afdbe05037499 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Sat, 20 Oct 2018 15:09:04 -0400 Subject: [PATCH 01/27] Use `type(self)` as a constructor --- ppb_vector/vector2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ppb_vector/vector2.py b/ppb_vector/vector2.py index f241f4b6..e6af43b5 100644 --- a/ppb_vector/vector2.py +++ b/ppb_vector/vector2.py @@ -75,7 +75,7 @@ def __mul__(self, other: VectorLike) -> 'Vector2': return NotImplemented return self.x * other.x + self.y * other.y elif isinstance(other, Real): - return Vector2(self.x * other, self.y * other) + return type(self)(self.x * other, self.y * other) else: return NotImplemented @@ -175,7 +175,7 @@ def rotate(self, degrees: Real) -> 'Vector2': x = self.x * r_cos - self.y * r_sin y = self.x * r_sin + self.y * r_cos - return Vector2(x, y) + return type(self)(x, y) def normalize(self) -> 'Vector2': return self.scale(1) From 11aec9d489e4b95fe1acf3d33c1775e035095503 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Sat, 20 Oct 2018 15:10:58 -0400 Subject: [PATCH 02/27] Improve the algorithm for selecting a type when two subclasses are given --- ppb_vector/vector2.py | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/ppb_vector/vector2.py b/ppb_vector/vector2.py index e6af43b5..ac4fcadc 100644 --- a/ppb_vector/vector2.py +++ b/ppb_vector/vector2.py @@ -1,5 +1,6 @@ import typing import collections +import functools from math import acos, atan2, cos, degrees, hypot, isclose, radians, sin from numbers import Real from collections.abc import Sequence @@ -34,6 +35,35 @@ def _mkvector(value, *, castto=_fakevector): raise ValueError(f"Cannot use {value} as a vector-like") +@functools.lru_cache() +def _find_lowest_type(left: type, right: type) -> type: + """ + Guess which is the more specific type. + """ + # Basically, see what classes are unique in each type's MRO and return who + # 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): + return right + else: + # They're equal, just arbitrarily pick one + return left + + +def _find_lowest_vector(left: type, right: type) -> type: + if not issubclass(left, Vector2): + return right + elif not issubclass(right, Vector2): + return left + else: + return _find_lowest_type(left, right) + + class Vector2(Sequence): def __init__(self, x: Real, y: Real): @@ -56,7 +86,7 @@ def __add__(self, other: VectorLike) -> 'Vector2': other = _mkvector(other) except ValueError: return NotImplemented - rtype = type(other) if isinstance(other, Vector2) else type(self) + rtype = _find_lowest_vector(type(other), type(self)) return rtype(self.x + other.x, self.y + other.y) def __sub__(self, other: VectorLike) -> 'Vector2': @@ -64,7 +94,7 @@ def __sub__(self, other: VectorLike) -> 'Vector2': other = _mkvector(other) except ValueError: return NotImplemented - rtype = type(other) if isinstance(other, Vector2) else type(self) + rtype = _find_lowest_vector(type(other), type(self)) return rtype(self.x - other.x, self.y - other.y) def __mul__(self, other: VectorLike) -> 'Vector2': From fe2462bd2c4466eb57746be979a3c1ce9eb5f9f8 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Sat, 20 Oct 2018 15:12:17 -0400 Subject: [PATCH 03/27] Handle the common case when the two vectors are the same type --- ppb_vector/vector2.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ppb_vector/vector2.py b/ppb_vector/vector2.py index ac4fcadc..d78ded2c 100644 --- a/ppb_vector/vector2.py +++ b/ppb_vector/vector2.py @@ -56,7 +56,9 @@ def _find_lowest_type(left: type, right: type) -> type: def _find_lowest_vector(left: type, right: type) -> type: - if not issubclass(left, Vector2): + if left is right: + return left + elif not issubclass(left, Vector2): return right elif not issubclass(right, Vector2): return left From 678be08dcb9970b31c12d5e6a91a003bc41520df Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Sat, 20 Oct 2018 15:13:59 -0400 Subject: [PATCH 04/27] Fix some annotations --- ppb_vector/vector2.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ppb_vector/vector2.py b/ppb_vector/vector2.py index d78ded2c..d803e0e6 100644 --- a/ppb_vector/vector2.py +++ b/ppb_vector/vector2.py @@ -99,7 +99,7 @@ def __sub__(self, other: VectorLike) -> 'Vector2': rtype = _find_lowest_vector(type(other), type(self)) return rtype(self.x - other.x, self.y - other.y) - def __mul__(self, other: VectorLike) -> 'Vector2': + def __mul__(self, other: VectorLike) -> typing.Union['Vector2', Real]: if is_vector_like(other): try: other = _mkvector(other) @@ -111,7 +111,7 @@ def __mul__(self, other: VectorLike) -> 'Vector2': else: return NotImplemented - def __rmul__(self, other: VectorLike) -> 'Vector2': + def __rmul__(self, other: VectorLike) -> typing.Union['Vector2', Real]: return self.__mul__(other) def __xor__(self, other: VectorLike) -> Real: @@ -178,7 +178,7 @@ def angle(self, other: VectorLike) -> Real: return rv - def isclose(self, other: 'Vector2', *, rel_tol: float=1e-06, abs_tol: float=1e-3): + def isclose(self, other: VectorLike, *, rel_tol: float=1e-06, abs_tol: float=1e-3): """ Determine whether two vectors are close in value. From f9d6eb9280858a6226239edcc12f2a48393f4024 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Sat, 20 Oct 2018 15:25:57 -0400 Subject: [PATCH 05/27] Fix some more annotations --- ppb_vector/vector2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ppb_vector/vector2.py b/ppb_vector/vector2.py index d803e0e6..661f4064 100644 --- a/ppb_vector/vector2.py +++ b/ppb_vector/vector2.py @@ -99,7 +99,7 @@ def __sub__(self, other: VectorLike) -> 'Vector2': rtype = _find_lowest_vector(type(other), type(self)) return rtype(self.x - other.x, self.y - other.y) - def __mul__(self, other: VectorLike) -> typing.Union['Vector2', Real]: + def __mul__(self, other: typing.Union[VectorLike, Real]) -> typing.Union['Vector2', Real]: if is_vector_like(other): try: other = _mkvector(other) @@ -111,7 +111,7 @@ def __mul__(self, other: VectorLike) -> typing.Union['Vector2', Real]: else: return NotImplemented - def __rmul__(self, other: VectorLike) -> typing.Union['Vector2', Real]: + def __rmul__(self, other: typing.Union[VectorLike, Real]) -> typing.Union['Vector2', Real]: return self.__mul__(other) def __xor__(self, other: VectorLike) -> Real: From 06f64c8244732c22aa9ace5ffd5270c52751e7a3 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Sat, 20 Oct 2018 15:30:09 -0400 Subject: [PATCH 06/27] Add tests for typing --- tests/test_typing.py | 77 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 tests/test_typing.py diff --git a/tests/test_typing.py b/tests/test_typing.py new file mode 100644 index 00000000..16c432fb --- /dev/null +++ b/tests/test_typing.py @@ -0,0 +1,77 @@ +import pytest + +from ppb_vector import Vector2 + +# List of operations that (Vector2, Vector2) -> Vector2 +BINOPS = [ + Vector2.__add__, + Vector2.__sub__, + Vector2.reflect, +] + +# List of operations that (Vector2, Real) -> Vector2 +VNUMOPS = [ + Vector2.__mul__, + Vector2.__rmul__, + Vector2.rotate, + Vector2.truncate, + Vector2.scale, +] + +# List of operations that (Vector2) -> Vector2 +MONOPS = [ + Vector2.convert, + Vector2.__neg__, + Vector2.normalize, +] + +@pytest.mark.parametrize('op', BINOPS) +def test_binop_same(op): + class V(Vector2): pass + + # Normalize for reflect + a = op(V(1, 2), V(3, 4).normalize()) + + assert isinstance(a, V) + + +@pytest.mark.parametrize('op', BINOPS) +def test_binop_different(op): + class V1(Vector2): pass + class V2(Vector2): pass + + # Normalize for reflect + a = op(V1(1, 2), V2(3, 4).normalize()) + b = op(V2(1, 2), V1(3, 4).normalize()) + assert isinstance(a, (V1, V2)) + assert isinstance(b, (V1, V2)) + + +@pytest.mark.parametrize('op', BINOPS) +def test_binop_subclass(op): + class V1(Vector2): pass + class V2(V1): pass + + # Normalize for reflect + a = op(V1(1, 2), V2(3, 4).normalize()) + b = op(V2(1, 2), V1(3, 4).normalize()) + assert isinstance(a, V2) + assert isinstance(b, V2) + + +@pytest.mark.parametrize('op', VNUMOPS) +def test_vnumop(op): + class V(Vector2): pass + + a = op(V(1, 2), 42) + + assert isinstance(a, V) + + +@pytest.mark.parametrize('op', MONOPS) +def test_monop(op): + class V(Vector2): pass + + a = op(V(1, 2)) + + assert isinstance(a, V) From 2ae4f564e7a56cdb947320cb88d24290f066cfc7 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Sat, 20 Oct 2018 21:02:58 -0400 Subject: [PATCH 07/27] Use typing.Type instead of type --- ppb_vector/vector2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ppb_vector/vector2.py b/ppb_vector/vector2.py index 661f4064..e215c2ea 100644 --- a/ppb_vector/vector2.py +++ b/ppb_vector/vector2.py @@ -36,7 +36,7 @@ def _mkvector(value, *, castto=_fakevector): @functools.lru_cache() -def _find_lowest_type(left: type, right: type) -> type: +def _find_lowest_type(left: typing.Type, right: typing.Type) -> typing.Type: """ Guess which is the more specific type. """ @@ -55,7 +55,7 @@ def _find_lowest_type(left: type, right: type) -> type: return left -def _find_lowest_vector(left: type, right: type) -> type: +def _find_lowest_vector(left: typing.Type, right: typing.Type) -> typing.Type: if left is right: return left elif not issubclass(left, Vector2): From 0cee3a210e37808400941e09546dab31248bdca1 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Sat, 20 Oct 2018 21:05:21 -0400 Subject: [PATCH 08/27] test_typing: Use more explicit constant names --- tests/test_typing.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_typing.py b/tests/test_typing.py index 16c432fb..975df4ea 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -3,14 +3,14 @@ from ppb_vector import Vector2 # List of operations that (Vector2, Vector2) -> Vector2 -BINOPS = [ +BINARY_OPS = [ Vector2.__add__, Vector2.__sub__, Vector2.reflect, ] # List of operations that (Vector2, Real) -> Vector2 -VNUMOPS = [ +VECTOR_NUMBER_OPS = [ Vector2.__mul__, Vector2.__rmul__, Vector2.rotate, @@ -19,13 +19,13 @@ ] # List of operations that (Vector2) -> Vector2 -MONOPS = [ +UNARY_OPS = [ Vector2.convert, Vector2.__neg__, Vector2.normalize, ] -@pytest.mark.parametrize('op', BINOPS) +@pytest.mark.parametrize('op', BINARY_OPS) def test_binop_same(op): class V(Vector2): pass @@ -35,7 +35,7 @@ class V(Vector2): pass assert isinstance(a, V) -@pytest.mark.parametrize('op', BINOPS) +@pytest.mark.parametrize('op', BINARY_OPS) def test_binop_different(op): class V1(Vector2): pass class V2(Vector2): pass @@ -47,7 +47,7 @@ class V2(Vector2): pass assert isinstance(b, (V1, V2)) -@pytest.mark.parametrize('op', BINOPS) +@pytest.mark.parametrize('op', BINARY_OPS) def test_binop_subclass(op): class V1(Vector2): pass class V2(V1): pass @@ -59,7 +59,7 @@ class V2(V1): pass assert isinstance(b, V2) -@pytest.mark.parametrize('op', VNUMOPS) +@pytest.mark.parametrize('op', VECTOR_NUMBER_OPS) def test_vnumop(op): class V(Vector2): pass @@ -68,7 +68,7 @@ class V(Vector2): pass assert isinstance(a, V) -@pytest.mark.parametrize('op', MONOPS) +@pytest.mark.parametrize('op', UNARY_OPS) def test_monop(op): class V(Vector2): pass From f07e9234f55d4fdf3c0da929520284c2b695ac58 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Mon, 22 Oct 2018 17:28:10 -0400 Subject: [PATCH 09/27] Add subclass-aware annotations --- ppb_vector/vector2.py | 52 +++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/ppb_vector/vector2.py b/ppb_vector/vector2.py index e215c2ea..8166c56e 100644 --- a/ppb_vector/vector2.py +++ b/ppb_vector/vector2.py @@ -8,12 +8,16 @@ __all__ = 'Vector2', -VectorLike = typing.Union[ - 'Vector2', +# Vector or subclass +VectorOrSub = typing.TypeVar('VectorOrSub', bound='Vector2') + +# Anything convertable to a Vector, including lists, tuples, and dicts +VectorLike = typing.TypeVar('VectorLike', typing.Union[ + VectorOrSub, typing.List[Real], # TODO: Length 2 typing.Tuple[Real, Real], typing.Dict[str, Real], # TODO: Length 2, keys 'x', 'y' -] +]) def is_vector_like(value: typing.Any) -> bool: @@ -68,22 +72,22 @@ def _find_lowest_vector(left: typing.Type, right: typing.Type) -> typing.Type: class Vector2(Sequence): - def __init__(self, x: Real, y: Real): + def __init__(self: VectorOrSub, x: Real, y: Real): self.x = x self.y = y self.length = hypot(x, y) @classmethod - def convert(cls, value: VectorLike) -> 'Vector2': + def convert(cls: typing.Type[VectorOrSub], value: VectorLike) -> VectorOrSub: """ Constructs a vector from a vector-like. """ return _mkvector(value, castto=type(cls)) - def __len__(self) -> int: + def __len__(self: VectorOrSub) -> int: return 2 - def __add__(self, other: VectorLike) -> 'Vector2': + def __add__(self: VectorOrSub, other: VectorLike) -> VectorOrSub: try: other = _mkvector(other) except ValueError: @@ -91,7 +95,7 @@ def __add__(self, other: VectorLike) -> 'Vector2': rtype = _find_lowest_vector(type(other), type(self)) return rtype(self.x + other.x, self.y + other.y) - def __sub__(self, other: VectorLike) -> 'Vector2': + def __sub__(self: VectorOrSub, other: VectorLike) -> VectorOrSub: try: other = _mkvector(other) except ValueError: @@ -99,7 +103,7 @@ def __sub__(self, other: VectorLike) -> 'Vector2': rtype = _find_lowest_vector(type(other), type(self)) return rtype(self.x - other.x, self.y - other.y) - def __mul__(self, other: typing.Union[VectorLike, Real]) -> typing.Union['Vector2', Real]: + def __mul__(self: VectorOrSub, other: typing.Union[VectorLike, Real]) -> typing.Union[VectorOrSub, Real]: if is_vector_like(other): try: other = _mkvector(other) @@ -111,17 +115,17 @@ def __mul__(self, other: typing.Union[VectorLike, Real]) -> typing.Union['Vector else: return NotImplemented - def __rmul__(self, other: typing.Union[VectorLike, Real]) -> typing.Union['Vector2', Real]: + def __rmul__(self: VectorOrSub, other: typing.Union[VectorLike, Real]) -> typing.Union[VectorOrSub, Real]: return self.__mul__(other) - def __xor__(self, other: VectorLike) -> Real: + def __xor__(self: VectorOrSub, other: VectorLike) -> Real: """ Computes the magnitude of the cross product """ other = _mkvector(other) return self.x * other.y - self.y * other.x - def __getitem__(self, item: typing.Union[str, int]) -> Real: + def __getitem__(self: VectorOrSub, item: typing.Union[str, int]) -> Real: if hasattr(item, '__index__'): item = item.__index__() if isinstance(item, str): @@ -141,31 +145,31 @@ def __getitem__(self, item: typing.Union[str, int]) -> Real: else: raise TypeError - def __repr__(self) -> str: + def __repr__(self: VectorOrSub) -> str: return f"{type(self).__name__}({self.x}, {self.y})" - def __eq__(self, other: VectorLike) -> bool: + def __eq__(self: VectorOrSub, other: VectorLike) -> bool: if is_vector_like(other): other = _mkvector(other) return self.x == other.x and self.y == other.y else: return False - def __ne__(self, other: VectorLike) -> bool: + def __ne__(self: VectorOrSub, other: VectorLike) -> bool: if is_vector_like(other): other = _mkvector(other) return self.x != other.x or self.y != other.y else: return True - def __iter__(self) -> typing.Iterator[Real]: + def __iter__(self: VectorOrSub) -> typing.Iterator[Real]: yield self.x yield self.y - def __neg__(self) -> 'Vector2': + def __neg__(self: VectorOrSub) -> VectorOrSub: return self * -1 - def angle(self, other: VectorLike) -> Real: + def angle(self: VectorOrSub, other: VectorLike) -> Real: other = _mkvector(other, castto=Vector2) rv = degrees( atan2(other.x, -other.y) - atan2(self.x, -self.y) ) @@ -178,7 +182,7 @@ def angle(self, other: VectorLike) -> Real: return rv - def isclose(self, other: VectorLike, *, rel_tol: float=1e-06, abs_tol: float=1e-3): + def isclose(self: VectorOrSub, other: VectorLike, *, rel_tol: Real=1e-06, abs_tol: Real=1e-3) -> bool: """ Determine whether two vectors are close in value. @@ -200,7 +204,7 @@ def isclose(self, other: VectorLike, *, rel_tol: float=1e-06, abs_tol: float=1e- diff < abs_tol ) - def rotate(self, degrees: Real) -> 'Vector2': + def rotate(self: VectorOrSub, degrees: Real) -> VectorOrSub: r = radians(degrees) r_cos = cos(r) r_sin = sin(r) @@ -209,22 +213,22 @@ def rotate(self, degrees: Real) -> 'Vector2': y = self.x * r_sin + self.y * r_cos return type(self)(x, y) - def normalize(self) -> 'Vector2': + def normalize(self: VectorOrSub) -> VectorOrSub: return self.scale(1) - def truncate(self, max_length: Real) -> 'Vector2': + def truncate(self: VectorOrSub, max_length: Real) -> VectorOrSub: if self.length > max_length: return self.scale(max_length) return self - def scale(self, length: Real) -> 'Vector2': + def scale(self: VectorOrSub, length: Real) -> VectorOrSub: try: scale = length / self.length except ZeroDivisionError: scale = 1 return self * scale - def reflect(self, surface_normal: VectorLike) -> 'Vector2': + def reflect(self: VectorOrSub, surface_normal: VectorLike) -> VectorOrSub: """ Calculate the reflection of the vector against a given surface normal """ From 118abfc0bc38bb2ac617c0e13b513524d46c5ed1 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Mon, 22 Oct 2018 17:44:43 -0400 Subject: [PATCH 10/27] Run mypy in travis --- .travis.yml | 1 + dev-requirements.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index b2cbefa4..7f963766 100644 --- a/.travis.yml +++ b/.travis.yml @@ -36,3 +36,4 @@ install: script: - pytest + - mypy ppb_vector tests diff --git a/dev-requirements.txt b/dev-requirements.txt index ba545d17..5591de24 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,2 +1,3 @@ pytest~=3.8 hypothesis +mypy From 86858d3ff493bf2d0b5d3661499411adba141d5c Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Mon, 22 Oct 2018 18:05:03 -0400 Subject: [PATCH 11/27] Bunch of mypy refactoring, add `scale_by` and `scale_to`. --- .gitignore | 1 + ppb_vector/vector2.py | 65 +++++++++++++++++++++++++++++++------------ 2 files changed, 48 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index 2726f519..4aeaa48e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /*.egg-info/ __pycache__/ .hypothesis/ +/.mypy_cache/ diff --git a/ppb_vector/vector2.py b/ppb_vector/vector2.py index 8166c56e..daf629c1 100644 --- a/ppb_vector/vector2.py +++ b/ppb_vector/vector2.py @@ -12,12 +12,14 @@ VectorOrSub = typing.TypeVar('VectorOrSub', bound='Vector2') # Anything convertable to a Vector, including lists, tuples, and dicts -VectorLike = typing.TypeVar('VectorLike', typing.Union[ +VectorLike = typing.Union[ VectorOrSub, typing.List[Real], # TODO: Length 2 typing.Tuple[Real, Real], typing.Dict[str, Real], # TODO: Length 2, keys 'x', 'y' -]) +] + +Realish = typing.Union[Real, float, int] def is_vector_like(value: typing.Any) -> bool: @@ -70,9 +72,12 @@ def _find_lowest_vector(left: typing.Type, right: typing.Type) -> typing.Type: return _find_lowest_type(left, right) -class Vector2(Sequence): +class Vector2: + x: Realish + y: Realish + length: float - def __init__(self: VectorOrSub, x: Real, y: Real): + def __init__(self: VectorOrSub, x: Realish, y: Realish): self.x = x self.y = y self.length = hypot(x, y) @@ -103,19 +108,34 @@ def __sub__(self: VectorOrSub, other: VectorLike) -> VectorOrSub: rtype = _find_lowest_vector(type(other), type(self)) return rtype(self.x - other.x, self.y - other.y) - def __mul__(self: VectorOrSub, other: typing.Union[VectorLike, Real]) -> typing.Union[VectorOrSub, Real]: + def dot(self: VectorOrSub, other: VectorLike) -> Realish: + """ + Return the dot product of two vectors. + """ + other = _mkvector(other) + return self.x * other.x + self.y * other.y + + def scale_by(self: VectorOrSub, other: Realish) -> VectorOrSub: + """ + Scale by the given amount. + """ + return type(self)(self.x * other, self.y * other) + + def __mul__(self: VectorOrSub, other: typing.Union[VectorLike, Realish]) -> typing.Union[VectorOrSub, Realish]: + """ + Performs a dot product or scale based on other. + """ if is_vector_like(other): try: - other = _mkvector(other) + return self.dot(other) except ValueError: return NotImplemented - return self.x * other.x + self.y * other.y elif isinstance(other, Real): - return type(self)(self.x * other, self.y * other) + return self.scale_by(other) else: return NotImplemented - def __rmul__(self: VectorOrSub, other: typing.Union[VectorLike, Real]) -> typing.Union[VectorOrSub, Real]: + def __rmul__(self: VectorOrSub, other: typing.Union[VectorLike, Realish]) -> typing.Union[VectorOrSub, Realish]: return self.__mul__(other) def __xor__(self: VectorOrSub, other: VectorLike) -> Real: @@ -125,7 +145,7 @@ def __xor__(self: VectorOrSub, other: VectorLike) -> Real: other = _mkvector(other) return self.x * other.y - self.y * other.x - def __getitem__(self: VectorOrSub, item: typing.Union[str, int]) -> Real: + def __getitem__(self: VectorOrSub, item: typing.Union[str, int]) -> Realish: if hasattr(item, '__index__'): item = item.__index__() if isinstance(item, str): @@ -162,14 +182,14 @@ def __ne__(self: VectorOrSub, other: VectorLike) -> bool: else: return True - def __iter__(self: VectorOrSub) -> typing.Iterator[Real]: + def __iter__(self: VectorOrSub) -> typing.Iterator[Realish]: yield self.x yield self.y def __neg__(self: VectorOrSub) -> VectorOrSub: - return self * -1 + return self.scale_by(-1) - def angle(self: VectorOrSub, other: VectorLike) -> Real: + def angle(self: VectorOrSub, other: VectorLike) -> Realish: other = _mkvector(other, castto=Vector2) rv = degrees( atan2(other.x, -other.y) - atan2(self.x, -self.y) ) @@ -182,7 +202,7 @@ def angle(self: VectorOrSub, other: VectorLike) -> Real: return rv - def isclose(self: VectorOrSub, other: VectorLike, *, rel_tol: Real=1e-06, abs_tol: Real=1e-3) -> bool: + def isclose(self: VectorOrSub, other: VectorLike, *, rel_tol: Realish=1e-06, abs_tol: Realish=1e-3) -> bool: """ Determine whether two vectors are close in value. @@ -198,13 +218,15 @@ def isclose(self: VectorOrSub, other: VectorLike, *, rel_tol: Real=1e-06, abs_to For the values to be considered close, the difference between them must be smaller than at least one of the tolerances. """ + other = _mkvector(other, castto=Vector2) diff = (self - other).length return ( - diff < rel_tol * max(self.length, other.length) or + diff < rel_tol * self.length or + diff < rel_tol * other.length or diff < abs_tol ) - def rotate(self: VectorOrSub, degrees: Real) -> VectorOrSub: + def rotate(self: VectorOrSub, degrees: Realish) -> VectorOrSub: r = radians(degrees) r_cos = cos(r) r_sin = sin(r) @@ -216,18 +238,23 @@ def rotate(self: VectorOrSub, degrees: Real) -> VectorOrSub: def normalize(self: VectorOrSub) -> VectorOrSub: return self.scale(1) - def truncate(self: VectorOrSub, max_length: Real) -> VectorOrSub: + def truncate(self: VectorOrSub, max_length: Realish) -> VectorOrSub: if self.length > max_length: return self.scale(max_length) return self - def scale(self: VectorOrSub, length: Real) -> VectorOrSub: + def scale_to(self: VectorOrSub, length: Real) -> VectorOrSub: + """ + Scale the vector to the given length + """ try: scale = length / self.length except ZeroDivisionError: scale = 1 return self * scale + scale = scale_to + def reflect(self: VectorOrSub, surface_normal: VectorLike) -> VectorOrSub: """ Calculate the reflection of the vector against a given surface normal @@ -237,3 +264,5 @@ def reflect(self: VectorOrSub, surface_normal: VectorLike) -> VectorOrSub: raise ValueError("Reflection requires a normalized vector.") return self - (2 * (self * surface_normal) * surface_normal) + +Sequence.register(Vector2) From 40b8ee5e0e4f2b2441ca76952da6df8811ae7a64 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Mon, 22 Oct 2018 18:16:12 -0400 Subject: [PATCH 12/27] Fix convert, add tests --- ppb_vector/vector2.py | 3 ++- tests/test_typing.py | 2 +- tests/test_vector2.py | 25 +++++++++++++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/ppb_vector/vector2.py b/ppb_vector/vector2.py index daf629c1..74e59a17 100644 --- a/ppb_vector/vector2.py +++ b/ppb_vector/vector2.py @@ -87,7 +87,8 @@ def convert(cls: typing.Type[VectorOrSub], value: VectorLike) -> VectorOrSub: """ Constructs a vector from a vector-like. """ - return _mkvector(value, castto=type(cls)) + fake = _mkvector(value) + return cls(fake.x, fake.y) def __len__(self: VectorOrSub) -> int: return 2 diff --git a/tests/test_typing.py b/tests/test_typing.py index 975df4ea..67d151de 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -20,7 +20,7 @@ # List of operations that (Vector2) -> Vector2 UNARY_OPS = [ - Vector2.convert, + lambda v: type(v).convert(v), Vector2.__neg__, Vector2.normalize, ] diff --git a/tests/test_vector2.py b/tests/test_vector2.py index 146d5ce7..6caad17f 100644 --- a/tests/test_vector2.py +++ b/tests/test_vector2.py @@ -18,3 +18,28 @@ def test_is_iterable(): @pytest.mark.parametrize('test_vector, expected_result', negation_data) def test_negation(test_vector, expected_result): assert -test_vector == expected_result + + +@pytest.mark.parametrize('value', [ + Vector2(1, 2), + [3, 4], + (5, 6), + {'x': 7, 'y': 8}, +]) +def test_convert(value): + v = Vector2.convert(value) + assert isinstance(v, Vector2) + assert v == value + + +@pytest.mark.parametrize('value', [ + Vector2(1, 2), + [3, 4], + (5, 6), + {'x': 7, 'y': 8}, +]) +def test_convert_subclass(value): + class V(Vector2): pass + v = V.convert(value) + assert isinstance(v, V) + assert v == value From dc21322a29c90f21c126a100b9a0681e096ebabc Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Mon, 22 Oct 2018 18:24:00 -0400 Subject: [PATCH 13/27] Tell mypy to ignore pytest --- tests/test_typing.py | 2 +- tests/test_vector2.py | 2 +- tests/test_vector2_addition.py | 2 +- tests/test_vector2_angle.py | 2 +- tests/test_vector2_cross.py | 2 +- tests/test_vector2_length.py | 2 +- tests/test_vector2_member_access.py | 2 +- tests/test_vector2_normalize.py | 2 +- tests/test_vector2_reflect.py | 2 +- tests/test_vector2_rotate.py | 2 +- tests/test_vector2_scalar_multiplication.py | 2 +- tests/test_vector2_scale.py | 2 +- tests/test_vector2_substraction.py | 2 +- tests/test_vector2_truncate.py | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/test_typing.py b/tests/test_typing.py index 67d151de..b4dfaa0a 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -1,4 +1,4 @@ -import pytest +import pytest # type: ignore from ppb_vector import Vector2 diff --git a/tests/test_vector2.py b/tests/test_vector2.py index 6caad17f..aba4b2a2 100644 --- a/tests/test_vector2.py +++ b/tests/test_vector2.py @@ -1,4 +1,4 @@ -import pytest +import pytest # type: ignore from ppb_vector import Vector2 diff --git a/tests/test_vector2_addition.py b/tests/test_vector2_addition.py index b653cc5a..9cb6a827 100644 --- a/tests/test_vector2_addition.py +++ b/tests/test_vector2_addition.py @@ -1,4 +1,4 @@ -import pytest +import pytest # type: ignore from ppb_vector import Vector2 diff --git a/tests/test_vector2_angle.py b/tests/test_vector2_angle.py index ace5fe36..eac3342c 100644 --- a/tests/test_vector2_angle.py +++ b/tests/test_vector2_angle.py @@ -1,6 +1,6 @@ from ppb_vector import Vector2 from math import isclose -import pytest +import pytest # type: ignore from hypothesis import assume, given, note from utils import angle_isclose, vectors diff --git a/tests/test_vector2_cross.py b/tests/test_vector2_cross.py index 35f17cc1..e4838fe8 100644 --- a/tests/test_vector2_cross.py +++ b/tests/test_vector2_cross.py @@ -1,5 +1,5 @@ from ppb_vector import Vector2 -import pytest +import pytest # type: ignore @pytest.mark.parametrize("left, right, expected", [ (Vector2(1, 1), Vector2(0, -1), -1), diff --git a/tests/test_vector2_length.py b/tests/test_vector2_length.py index 219d0bc9..5a04b328 100644 --- a/tests/test_vector2_length.py +++ b/tests/test_vector2_length.py @@ -1,5 +1,5 @@ import ppb_vector -import pytest +import pytest # type: ignore @pytest.mark.parametrize("x, y, expected", [ diff --git a/tests/test_vector2_member_access.py b/tests/test_vector2_member_access.py index 8b31b195..3a282de1 100644 --- a/tests/test_vector2_member_access.py +++ b/tests/test_vector2_member_access.py @@ -1,4 +1,4 @@ -import pytest +import pytest # type: ignore from ppb_vector import Vector2 diff --git a/tests/test_vector2_normalize.py b/tests/test_vector2_normalize.py index 12743bde..f315f925 100644 --- a/tests/test_vector2_normalize.py +++ b/tests/test_vector2_normalize.py @@ -1,4 +1,4 @@ -import pytest +import pytest # type: ignore import ppb_vector diff --git a/tests/test_vector2_reflect.py b/tests/test_vector2_reflect.py index 350f864c..b484b646 100644 --- a/tests/test_vector2_reflect.py +++ b/tests/test_vector2_reflect.py @@ -1,5 +1,5 @@ from ppb_vector import Vector2 -import pytest +import pytest # type: ignore from hypothesis import given, assume, note from math import isclose, isinf from utils import angle_isclose, units, vectors diff --git a/tests/test_vector2_rotate.py b/tests/test_vector2_rotate.py index 31d38990..232b47a3 100644 --- a/tests/test_vector2_rotate.py +++ b/tests/test_vector2_rotate.py @@ -1,6 +1,6 @@ from ppb_vector import Vector2 from utils import angle_isclose, vectors -import pytest +import pytest # type: ignore import math from hypothesis import assume, given, note import hypothesis.strategies as st diff --git a/tests/test_vector2_scalar_multiplication.py b/tests/test_vector2_scalar_multiplication.py index 2e0fcef9..4251b428 100644 --- a/tests/test_vector2_scalar_multiplication.py +++ b/tests/test_vector2_scalar_multiplication.py @@ -1,4 +1,4 @@ -import pytest +import pytest # type: ignore from ppb_vector import Vector2 diff --git a/tests/test_vector2_scale.py b/tests/test_vector2_scale.py index 06c269a6..2b2be309 100644 --- a/tests/test_vector2_scale.py +++ b/tests/test_vector2_scale.py @@ -1,4 +1,4 @@ -import pytest +import pytest # type: ignore from math import hypot from ppb_vector import Vector2 diff --git a/tests/test_vector2_substraction.py b/tests/test_vector2_substraction.py index 715352b1..d38d8bdb 100644 --- a/tests/test_vector2_substraction.py +++ b/tests/test_vector2_substraction.py @@ -1,4 +1,4 @@ -import pytest +import pytest # type: ignore from ppb_vector import Vector2 diff --git a/tests/test_vector2_truncate.py b/tests/test_vector2_truncate.py index 606243ea..55d52aed 100644 --- a/tests/test_vector2_truncate.py +++ b/tests/test_vector2_truncate.py @@ -1,4 +1,4 @@ -import pytest +import pytest # type: ignore from ppb_vector import Vector2 From 8a2d1816706276ed382e52cc6d4dbc9303e24990 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Mon, 22 Oct 2018 18:24:19 -0400 Subject: [PATCH 14/27] mypy: Shut up about __index__ --- ppb_vector/vector2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ppb_vector/vector2.py b/ppb_vector/vector2.py index 74e59a17..0b23d6ce 100644 --- a/ppb_vector/vector2.py +++ b/ppb_vector/vector2.py @@ -148,7 +148,7 @@ def __xor__(self: VectorOrSub, other: VectorLike) -> Real: def __getitem__(self: VectorOrSub, item: typing.Union[str, int]) -> Realish: if hasattr(item, '__index__'): - item = item.__index__() + item = item.__index__() # type: ignore if isinstance(item, str): if item == 'x': return self.x @@ -241,7 +241,7 @@ def normalize(self: VectorOrSub) -> VectorOrSub: def truncate(self: VectorOrSub, max_length: Realish) -> VectorOrSub: if self.length > max_length: - return self.scale(max_length) + return self.scale_to(max_length) return self def scale_to(self: VectorOrSub, length: Real) -> VectorOrSub: From e87798fd2b5973084c16edaec76632f5618cfb7d Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Mon, 22 Oct 2018 18:29:56 -0400 Subject: [PATCH 15/27] Fix angle tests --- tests/test_vector2_angle.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_vector2_angle.py b/tests/test_vector2_angle.py index eac3342c..2913d9b3 100644 --- a/tests/test_vector2_angle.py +++ b/tests/test_vector2_angle.py @@ -10,7 +10,7 @@ (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, -1), Vector2(-1, 0), -45), (Vector2(1, 0), Vector2(0, 1), 90), (Vector2(1, 0), Vector2(1, 0), 0), ]) @@ -20,14 +20,14 @@ def test_angle(left, right, expected): assert -180 < lr <= 180 assert -180 < rl <= 180 assert isclose(lr, expected) - assert isclose(rl, -expected) + assert isclose(rl, 180 if expected == 180 else -expected) @given( left=vectors(), right=vectors(), ) -def test_angle(left, right): +def test_angle_prop(left, right): lr = left.angle(right) rl = right.angle(left) assert -180 < lr <= 180 From a0557430706019b1b8eb5e3f219224927999467a Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Mon, 22 Oct 2018 18:33:39 -0400 Subject: [PATCH 16/27] More annotation fixups --- ppb_vector/vector2.py | 8 ++++---- tests/test_typing.py | 5 ++--- tests/test_vector2_reflect.py | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/ppb_vector/vector2.py b/ppb_vector/vector2.py index 0b23d6ce..8ce9577f 100644 --- a/ppb_vector/vector2.py +++ b/ppb_vector/vector2.py @@ -19,7 +19,7 @@ typing.Dict[str, Real], # TODO: Length 2, keys 'x', 'y' ] -Realish = typing.Union[Real, float, int] +Realish = typing.Union[Real, float, int, typing.SupportsFloat] def is_vector_like(value: typing.Any) -> bool: @@ -109,7 +109,7 @@ def __sub__(self: VectorOrSub, other: VectorLike) -> VectorOrSub: rtype = _find_lowest_vector(type(other), type(self)) return rtype(self.x - other.x, self.y - other.y) - def dot(self: VectorOrSub, other: VectorLike) -> Realish: + def dot(self: VectorOrSub, other: VectorLike) -> Real: """ Return the dot product of two vectors. """ @@ -190,7 +190,7 @@ def __iter__(self: VectorOrSub) -> typing.Iterator[Realish]: def __neg__(self: VectorOrSub) -> VectorOrSub: return self.scale_by(-1) - def angle(self: VectorOrSub, other: VectorLike) -> Realish: + def angle(self: VectorOrSub, other: VectorLike) -> Real: other = _mkvector(other, castto=Vector2) rv = degrees( atan2(other.x, -other.y) - atan2(self.x, -self.y) ) @@ -244,7 +244,7 @@ def truncate(self: VectorOrSub, max_length: Realish) -> VectorOrSub: return self.scale_to(max_length) return self - def scale_to(self: VectorOrSub, length: Real) -> VectorOrSub: + def scale_to(self: VectorOrSub, length: Realish) -> VectorOrSub: """ Scale the vector to the given length """ diff --git a/tests/test_typing.py b/tests/test_typing.py index b4dfaa0a..d55de6e9 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -11,11 +11,10 @@ # List of operations that (Vector2, Real) -> Vector2 VECTOR_NUMBER_OPS = [ - Vector2.__mul__, - Vector2.__rmul__, + Vector2.scale_by, Vector2.rotate, Vector2.truncate, - Vector2.scale, + Vector2.scale_to, ] # List of operations that (Vector2) -> Vector2 diff --git a/tests/test_vector2_reflect.py b/tests/test_vector2_reflect.py index b484b646..2c92dbc7 100644 --- a/tests/test_vector2_reflect.py +++ b/tests/test_vector2_reflect.py @@ -27,7 +27,7 @@ def test_reflect_prop(initial: Vector2, normal: Vector2): note(f"Reflected: {reflected}") assert not any(map(isinf, reflected)) assert initial.isclose(returned) - assert isclose((initial * normal), -(reflected * normal)) + assert isclose((initial.dot(normal)), -(reflected.dot(normal))) assert angle_isclose(normal.angle(initial), 180 - normal.angle(reflected) ) From 66ec631e63b7013f6b012334fd18dca31f4a38ab Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Mon, 22 Oct 2018 18:45:39 -0400 Subject: [PATCH 17/27] More typing updates --- ppb_vector/vector2.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ppb_vector/vector2.py b/ppb_vector/vector2.py index e84f7c0e..2773166d 100644 --- a/ppb_vector/vector2.py +++ b/ppb_vector/vector2.py @@ -19,7 +19,7 @@ typing.Dict[str, Real], # TODO: Length 2, keys 'x', 'y' ] -Realish = typing.Union[Real, float, int, typing.SupportsFloat] +Realish = typing.Union[Real, float, int] def is_vector_like(value: typing.Any) -> bool: @@ -73,13 +73,13 @@ def _find_lowest_vector(left: typing.Type, right: typing.Type) -> typing.Type: class Vector2: - x: Realish - y: Realish + x: float + y: float length: float def __init__(self: VectorOrSub, x: Realish, y: Realish): - self.x = x - self.y = y + self.x = float(x) + self.y = float(y) self.length = hypot(x, y) @classmethod @@ -190,7 +190,7 @@ def __iter__(self: VectorOrSub) -> typing.Iterator[Realish]: def __neg__(self: VectorOrSub) -> VectorOrSub: return self.scale_by(-1) - def angle(self: VectorOrSub, other: VectorLike) -> Real: + def angle(self: VectorOrSub, other: VectorLike) -> float: other = _mkvector(other, castto=Vector2) rv = degrees( atan2(other.x, -other.y) - atan2(self.x, -self.y) ) @@ -252,7 +252,7 @@ def scale_to(self: VectorOrSub, length: Realish) -> VectorOrSub: scale = length / self.length except ZeroDivisionError: scale = 1 - return self * scale + return self.scale_by(scale) scale = scale_to From b0196c5ee2e924ff5a8a779bb75486f94e51d905 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Mon, 22 Oct 2018 18:55:07 -0400 Subject: [PATCH 18/27] Better handling of types and errors in __init__ --- ppb_vector/vector2.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/ppb_vector/vector2.py b/ppb_vector/vector2.py index 2773166d..cfca34a2 100644 --- a/ppb_vector/vector2.py +++ b/ppb_vector/vector2.py @@ -73,14 +73,20 @@ def _find_lowest_vector(left: typing.Type, right: typing.Type) -> typing.Type: class Vector2: - x: float - y: float - length: float + x: Realish + y: Realish + length: Realish def __init__(self: VectorOrSub, x: Realish, y: Realish): - self.x = float(x) - self.y = float(y) - self.length = hypot(x, y) + try: + self.x = x.__float__() + except AttributeError: + raise TypeError(f"{type(x).__name__} object not convertable to float") + try: + self.y = y.__float__() + except AttributeError: + raise TypeError(f"{type(y).__name__} object not convertable to float") + self.length = hypot(self.x, self.y) @classmethod def convert(cls: typing.Type[VectorOrSub], value: VectorLike) -> VectorOrSub: From f4e0b7765c5ddf5ac7bcfb5324c6c4f76f0dc693 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Mon, 22 Oct 2018 18:59:20 -0400 Subject: [PATCH 19/27] Be specific in attributes, because It Just Helps --- ppb_vector/vector2.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ppb_vector/vector2.py b/ppb_vector/vector2.py index cfca34a2..faf71ca1 100644 --- a/ppb_vector/vector2.py +++ b/ppb_vector/vector2.py @@ -73,9 +73,9 @@ def _find_lowest_vector(left: typing.Type, right: typing.Type) -> typing.Type: class Vector2: - x: Realish - y: Realish - length: Realish + x: float + y: float + length: float def __init__(self: VectorOrSub, x: Realish, y: Realish): try: From a218218b37d7b5c0efb6cd4cc8f1a3ea148cbe0d Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Mon, 22 Oct 2018 21:53:14 -0400 Subject: [PATCH 20/27] Use overload --- ppb_vector/vector2.py | 54 ++++++++++++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/ppb_vector/vector2.py b/ppb_vector/vector2.py index faf71ca1..76fa941f 100644 --- a/ppb_vector/vector2.py +++ b/ppb_vector/vector2.py @@ -11,32 +11,36 @@ # Vector or subclass VectorOrSub = typing.TypeVar('VectorOrSub', bound='Vector2') +Realish = typing.Union[Real, float, int] + # Anything convertable to a Vector, including lists, tuples, and dicts VectorLike = typing.Union[ - VectorOrSub, - typing.List[Real], # TODO: Length 2 - typing.Tuple[Real, Real], - typing.Dict[str, Real], # TODO: Length 2, keys 'x', 'y' + 'Vector2', # Or subclasses, unconnected to the VectorOrSub typevar above + typing.List[typing.SupportsFloat], # TODO: Length 2 + typing.Tuple[typing.SupportsFloat, typing.SupportsFloat], + typing.Dict[str, typing.SupportsFloat], # TODO: Length 2, keys 'x', 'y' ] -Realish = typing.Union[Real, float, int] - def is_vector_like(value: typing.Any) -> bool: return isinstance(value, (Vector2, list, tuple, dict)) -_fakevector = collections.namedtuple('_fakevector', ['x', 'y']) +class _fakevector(typing.NamedTuple): + x: float + y: float -def _mkvector(value, *, castto=_fakevector): + +t_mkvector = typing.TypeVar('t_mkvector', 'Vector2', _fakevector, covariant=True) +def _mkvector(value: VectorLike, *, castto: typing.Type[t_mkvector]=_fakevector) -> t_mkvector: if isinstance(value, Vector2): - return value + return value # type: ignore # FIXME: Allow all types of sequences elif isinstance(value, (list, tuple)) and len(value) == 2: - return castto(value[0], value[1]) + return castto(value[0].__float__(), value[1].__float__()) # FIXME: Allow all types of mappings elif isinstance(value, dict) and 'x' in value and 'y' in value and len(value) == 2: - return castto(value['x'], value['y']) + return castto(value['x'].__float__(), value['y'].__float__()) else: raise ValueError(f"Cannot use {value} as a vector-like") @@ -77,7 +81,7 @@ class Vector2: y: float length: float - def __init__(self: VectorOrSub, x: Realish, y: Realish): + def __init__(self, x: typing.SupportsFloat, y: typing.SupportsFloat): try: self.x = x.__float__() except AttributeError: @@ -115,7 +119,7 @@ def __sub__(self: VectorOrSub, other: VectorLike) -> VectorOrSub: rtype = _find_lowest_vector(type(other), type(self)) return rtype(self.x - other.x, self.y - other.y) - def dot(self: VectorOrSub, other: VectorLike) -> Real: + def dot(self: VectorOrSub, other: VectorLike) -> Realish: """ Return the dot product of two vectors. """ @@ -128,7 +132,13 @@ def scale_by(self: VectorOrSub, other: Realish) -> VectorOrSub: """ return type(self)(self.x * other, self.y * other) - def __mul__(self: VectorOrSub, other: typing.Union[VectorLike, Realish]) -> typing.Union[VectorOrSub, Realish]: + @typing.overload + def __mul__(self: VectorOrSub, other: VectorLike) -> Realish: pass + + @typing.overload + def __mul__(self: VectorOrSub, other: Realish) -> VectorOrSub: pass + + def __mul__(self, other): """ Performs a dot product or scale based on other. """ @@ -142,10 +152,16 @@ def __mul__(self: VectorOrSub, other: typing.Union[VectorLike, Realish]) -> typi else: return NotImplemented - def __rmul__(self: VectorOrSub, other: typing.Union[VectorLike, Realish]) -> typing.Union[VectorOrSub, Realish]: + @typing.overload + def __rmul__(self: VectorOrSub, other: VectorLike) -> Realish: pass + + @typing.overload + def __rmul__(self: VectorOrSub, other: Realish) -> VectorOrSub: pass + + def __rmul__(self, other): return self.__mul__(other) - def __xor__(self: VectorOrSub, other: VectorLike) -> Real: + def __xor__(self: VectorOrSub, other: VectorLike) -> Realish: """ Computes the magnitude of the cross product """ @@ -175,14 +191,14 @@ def __getitem__(self: VectorOrSub, item: typing.Union[str, int]) -> Realish: def __repr__(self: VectorOrSub) -> str: return f"{type(self).__name__}({self.x}, {self.y})" - def __eq__(self: VectorOrSub, other: VectorLike) -> bool: + def __eq__(self: VectorOrSub, other: typing.Any) -> bool: if is_vector_like(other): other = _mkvector(other) return self.x == other.x and self.y == other.y else: return False - def __ne__(self: VectorOrSub, other: VectorLike) -> bool: + def __ne__(self: VectorOrSub, other: typing.Any) -> bool: if is_vector_like(other): other = _mkvector(other) return self.x != other.x or self.y != other.y @@ -230,7 +246,7 @@ def isclose(self: VectorOrSub, other: VectorLike, *, rel_tol: Realish=1e-06, abs return ( diff < rel_tol * self.length or diff < rel_tol * other.length or - diff < abs_tol + diff < float(abs_tol) ) def rotate(self: VectorOrSub, degrees: Realish) -> VectorOrSub: From 812cf6bbd6155aab9cf471b0d774bc5d8b95591f Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Mon, 22 Oct 2018 21:59:27 -0400 Subject: [PATCH 21/27] Move to lazy `.length`, nullifying the need for a bunch of code --- ppb_vector/vector2.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/ppb_vector/vector2.py b/ppb_vector/vector2.py index 76fa941f..feda20df 100644 --- a/ppb_vector/vector2.py +++ b/ppb_vector/vector2.py @@ -26,21 +26,15 @@ def is_vector_like(value: typing.Any) -> bool: return isinstance(value, (Vector2, list, tuple, dict)) -class _fakevector(typing.NamedTuple): - x: float - y: float - - -t_mkvector = typing.TypeVar('t_mkvector', 'Vector2', _fakevector, covariant=True) -def _mkvector(value: VectorLike, *, castto: typing.Type[t_mkvector]=_fakevector) -> t_mkvector: +def _mkvector(value: VectorLike) -> 'Vector2': if isinstance(value, Vector2): - return value # type: ignore + return value # FIXME: Allow all types of sequences elif isinstance(value, (list, tuple)) and len(value) == 2: - return castto(value[0].__float__(), value[1].__float__()) + return Vector2(value[0].__float__(), value[1].__float__()) # FIXME: Allow all types of mappings elif isinstance(value, dict) and 'x' in value and 'y' in value and len(value) == 2: - return castto(value['x'].__float__(), value['y'].__float__()) + return Vector2(value['x'].__float__(), value['y'].__float__()) else: raise ValueError(f"Cannot use {value} as a vector-like") @@ -79,7 +73,7 @@ def _find_lowest_vector(left: typing.Type, right: typing.Type) -> typing.Type: class Vector2: x: float y: float - length: float + _length: float def __init__(self, x: typing.SupportsFloat, y: typing.SupportsFloat): try: @@ -90,7 +84,12 @@ def __init__(self, x: typing.SupportsFloat, y: typing.SupportsFloat): self.y = y.__float__() except AttributeError: raise TypeError(f"{type(y).__name__} object not convertable to float") - self.length = hypot(self.x, self.y) + + @property + def length(self) -> float: + if not hasattr(self, '_length'): + self._length = hypot(self.x, self.y) + return self._length @classmethod def convert(cls: typing.Type[VectorOrSub], value: VectorLike) -> VectorOrSub: @@ -213,7 +212,7 @@ def __neg__(self: VectorOrSub) -> VectorOrSub: return self.scale_by(-1) def angle(self: VectorOrSub, other: VectorLike) -> float: - other = _mkvector(other, castto=Vector2) + other = _mkvector(other) rv = degrees( atan2(other.x, -other.y) - atan2(self.x, -self.y) ) # This normalizes the value to (-180, +180], which is the opposite of @@ -241,7 +240,7 @@ def isclose(self: VectorOrSub, other: VectorLike, *, rel_tol: Realish=1e-06, abs For the values to be considered close, the difference between them must be smaller than at least one of the tolerances. """ - other = _mkvector(other, castto=Vector2) + other = _mkvector(other) diff = (self - other).length return ( diff < rel_tol * self.length or @@ -282,7 +281,7 @@ def reflect(self: VectorOrSub, surface_normal: VectorLike) -> VectorOrSub: """ Calculate the reflection of the vector against a given surface normal """ - surface_normal = _mkvector(surface_normal, castto=Vector2) + surface_normal = _mkvector(surface_normal) if not isclose(surface_normal.length, 1): raise ValueError("Reflection requires a normalized vector.") From e3e2a46a12aa65801b2224e71c504b3d1b9fd970 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Mon, 22 Oct 2018 22:03:52 -0400 Subject: [PATCH 22/27] Fold _mkvector() into Vector2.convert() --- ppb_vector/vector2.py | 59 +++++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/ppb_vector/vector2.py b/ppb_vector/vector2.py index feda20df..75721fd9 100644 --- a/ppb_vector/vector2.py +++ b/ppb_vector/vector2.py @@ -26,19 +26,6 @@ def is_vector_like(value: typing.Any) -> bool: return isinstance(value, (Vector2, list, tuple, dict)) -def _mkvector(value: VectorLike) -> 'Vector2': - if isinstance(value, Vector2): - return value - # FIXME: Allow all types of sequences - elif isinstance(value, (list, tuple)) and len(value) == 2: - return Vector2(value[0].__float__(), value[1].__float__()) - # FIXME: Allow all types of mappings - elif isinstance(value, dict) and 'x' in value and 'y' in value and len(value) == 2: - return Vector2(value['x'].__float__(), value['y'].__float__()) - else: - raise ValueError(f"Cannot use {value} as a vector-like") - - @functools.lru_cache() def _find_lowest_type(left: typing.Type, right: typing.Type) -> typing.Type: """ @@ -85,26 +72,38 @@ def __init__(self, x: typing.SupportsFloat, y: typing.SupportsFloat): except AttributeError: raise TypeError(f"{type(y).__name__} object not convertable to float") - @property - def length(self) -> float: - if not hasattr(self, '_length'): - self._length = hypot(self.x, self.y) - return self._length - @classmethod def convert(cls: typing.Type[VectorOrSub], value: VectorLike) -> VectorOrSub: """ Constructs a vector from a vector-like. """ - fake = _mkvector(value) - return cls(fake.x, fake.y) + # Use Vector2.convert() instead of type(self).convert() so that + # _find_lowest_vector() can resolve things well. + if isinstance(value, cls): + return value + elif isinstance(value, Vector2): + return cls(value.x, value.y) + # FIXME: Allow all types of sequences + elif isinstance(value, (list, tuple)) and len(value) == 2: + return cls(value[0].__float__(), value[1].__float__()) + # FIXME: Allow all types of mappings + elif isinstance(value, dict) and 'x' in value and 'y' in value and len(value) == 2: + return cls(value['x'].__float__(), value['y'].__float__()) + else: + raise ValueError(f"Cannot use {value} as a vector-like") + + @property + def length(self) -> float: + if not hasattr(self, '_length'): + self._length = hypot(self.x, self.y) + return self._length def __len__(self: VectorOrSub) -> int: return 2 def __add__(self: VectorOrSub, other: VectorLike) -> VectorOrSub: try: - other = _mkvector(other) + other = Vector2.convert(other) except ValueError: return NotImplemented rtype = _find_lowest_vector(type(other), type(self)) @@ -112,7 +111,7 @@ def __add__(self: VectorOrSub, other: VectorLike) -> VectorOrSub: def __sub__(self: VectorOrSub, other: VectorLike) -> VectorOrSub: try: - other = _mkvector(other) + other = Vector2.convert(other) except ValueError: return NotImplemented rtype = _find_lowest_vector(type(other), type(self)) @@ -122,7 +121,7 @@ def dot(self: VectorOrSub, other: VectorLike) -> Realish: """ Return the dot product of two vectors. """ - other = _mkvector(other) + other = Vector2.convert(other) return self.x * other.x + self.y * other.y def scale_by(self: VectorOrSub, other: Realish) -> VectorOrSub: @@ -164,7 +163,7 @@ def __xor__(self: VectorOrSub, other: VectorLike) -> Realish: """ Computes the magnitude of the cross product """ - other = _mkvector(other) + other = Vector2.convert(other) return self.x * other.y - self.y * other.x def __getitem__(self: VectorOrSub, item: typing.Union[str, int]) -> Realish: @@ -192,14 +191,14 @@ def __repr__(self: VectorOrSub) -> str: def __eq__(self: VectorOrSub, other: typing.Any) -> bool: if is_vector_like(other): - other = _mkvector(other) + other = Vector2.convert(other) return self.x == other.x and self.y == other.y else: return False def __ne__(self: VectorOrSub, other: typing.Any) -> bool: if is_vector_like(other): - other = _mkvector(other) + other = Vector2.convert(other) return self.x != other.x or self.y != other.y else: return True @@ -212,7 +211,7 @@ def __neg__(self: VectorOrSub) -> VectorOrSub: return self.scale_by(-1) def angle(self: VectorOrSub, other: VectorLike) -> float: - other = _mkvector(other) + other = Vector2.convert(other) rv = degrees( atan2(other.x, -other.y) - atan2(self.x, -self.y) ) # This normalizes the value to (-180, +180], which is the opposite of @@ -240,7 +239,7 @@ def isclose(self: VectorOrSub, other: VectorLike, *, rel_tol: Realish=1e-06, abs For the values to be considered close, the difference between them must be smaller than at least one of the tolerances. """ - other = _mkvector(other) + other = Vector2.convert(other) diff = (self - other).length return ( diff < rel_tol * self.length or @@ -281,7 +280,7 @@ def reflect(self: VectorOrSub, surface_normal: VectorLike) -> VectorOrSub: """ Calculate the reflection of the vector against a given surface normal """ - surface_normal = _mkvector(surface_normal) + surface_normal = Vector2.convert(surface_normal) if not isclose(surface_normal.length, 1): raise ValueError("Reflection requires a normalized vector.") From 2052d8f8591a23450b197324be77da40c5353b0d Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Mon, 22 Oct 2018 22:05:22 -0400 Subject: [PATCH 23/27] Make sure to compute the return type before we cast other --- ppb_vector/vector2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ppb_vector/vector2.py b/ppb_vector/vector2.py index 75721fd9..0e729746 100644 --- a/ppb_vector/vector2.py +++ b/ppb_vector/vector2.py @@ -102,19 +102,19 @@ def __len__(self: VectorOrSub) -> int: return 2 def __add__(self: VectorOrSub, other: VectorLike) -> VectorOrSub: + rtype = _find_lowest_vector(type(other), type(self)) try: other = Vector2.convert(other) except ValueError: return NotImplemented - rtype = _find_lowest_vector(type(other), type(self)) return rtype(self.x + other.x, self.y + other.y) def __sub__(self: VectorOrSub, other: VectorLike) -> VectorOrSub: + rtype = _find_lowest_vector(type(other), type(self)) try: other = Vector2.convert(other) except ValueError: return NotImplemented - rtype = _find_lowest_vector(type(other), type(self)) return rtype(self.x - other.x, self.y - other.y) def dot(self: VectorOrSub, other: VectorLike) -> Realish: From a6161b9ea916fd893b4493fcf29d2b747ca1f360 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Tue, 30 Oct 2018 18:06:34 -0400 Subject: [PATCH 24/27] Use generic sequence/mapping --- ppb_vector/vector2.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/ppb_vector/vector2.py b/ppb_vector/vector2.py index 0e729746..ac33163e 100644 --- a/ppb_vector/vector2.py +++ b/ppb_vector/vector2.py @@ -3,7 +3,7 @@ import functools from math import acos, atan2, cos, degrees, hypot, isclose, radians, sin from numbers import Real -from collections.abc import Sequence +from collections.abc import Sequence, Mapping __all__ = 'Vector2', @@ -16,14 +16,14 @@ # Anything convertable to a Vector, including lists, tuples, and dicts VectorLike = typing.Union[ 'Vector2', # Or subclasses, unconnected to the VectorOrSub typevar above - typing.List[typing.SupportsFloat], # TODO: Length 2 typing.Tuple[typing.SupportsFloat, typing.SupportsFloat], - typing.Dict[str, typing.SupportsFloat], # TODO: Length 2, keys 'x', 'y' + typing.Sequence[typing.SupportsFloat], # TODO: Length 2 + typing.Mapping[str, typing.SupportsFloat], # TODO: Length 2, keys 'x', 'y' ] def is_vector_like(value: typing.Any) -> bool: - return isinstance(value, (Vector2, list, tuple, dict)) + return isinstance(value, (Vector2, Sequence, dict)) @functools.lru_cache() @@ -75,7 +75,7 @@ def __init__(self, x: typing.SupportsFloat, y: typing.SupportsFloat): @classmethod def convert(cls: typing.Type[VectorOrSub], value: VectorLike) -> VectorOrSub: """ - Constructs a vector from a vector-like. + Constructs a vector from a vector-like. Does not perform a copy. """ # Use Vector2.convert() instead of type(self).convert() so that # _find_lowest_vector() can resolve things well. @@ -83,11 +83,9 @@ def convert(cls: typing.Type[VectorOrSub], value: VectorLike) -> VectorOrSub: return value elif isinstance(value, Vector2): return cls(value.x, value.y) - # FIXME: Allow all types of sequences - elif isinstance(value, (list, tuple)) and len(value) == 2: + elif isinstance(value, Sequence) and len(value) == 2: return cls(value[0].__float__(), value[1].__float__()) - # FIXME: Allow all types of mappings - elif isinstance(value, dict) and 'x' in value and 'y' in value and len(value) == 2: + elif isinstance(value, Mapping) and 'x' in value and 'y' in value and len(value) == 2: return cls(value['x'].__float__(), value['y'].__float__()) else: raise ValueError(f"Cannot use {value} as a vector-like") From ef6ae72f8b27b972192ce79abc6d108596ffe2de Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Tue, 30 Oct 2018 18:10:20 -0400 Subject: [PATCH 25/27] Don't duplicate code into __ne__() --- ppb_vector/vector2.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/ppb_vector/vector2.py b/ppb_vector/vector2.py index ac33163e..9420773f 100644 --- a/ppb_vector/vector2.py +++ b/ppb_vector/vector2.py @@ -195,11 +195,7 @@ def __eq__(self: VectorOrSub, other: typing.Any) -> bool: return False def __ne__(self: VectorOrSub, other: typing.Any) -> bool: - if is_vector_like(other): - other = Vector2.convert(other) - return self.x != other.x or self.y != other.y - else: - return True + return not (self == other) def __iter__(self: VectorOrSub) -> typing.Iterator[Realish]: yield self.x From f5e23d7bc449900778e1201b0ae518a4d1b6da0c Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Tue, 30 Oct 2018 18:13:20 -0400 Subject: [PATCH 26/27] Undo unnecessary conversions to .dot() --- tests/test_vector2_reflect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_vector2_reflect.py b/tests/test_vector2_reflect.py index 95cccb49..4e642150 100644 --- a/tests/test_vector2_reflect.py +++ b/tests/test_vector2_reflect.py @@ -38,7 +38,7 @@ def test_reflect_prop(initial: Vector2, normal: Vector2): assert initial.isclose(returned) note(f"initial ⋅ normal: {initial * normal}") note(f"reflected ⋅ normal: {reflected * normal}") - assert isclose((initial.dot(normal)), -(reflected.dot(normal))) + assert isclose((initial * normal), -(reflected * normal)) assert angle_isclose(normal.angle(initial), 180 - normal.angle(reflected) ) From 5224a2ee1edc9b78dd6cc15c22746d39b4b705b1 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Thu, 1 Nov 2018 21:04:58 -0400 Subject: [PATCH 27/27] Don't cache the length, doesn't actually provide any benefits --- ppb_vector/vector2.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ppb_vector/vector2.py b/ppb_vector/vector2.py index 9420773f..cbaddb1a 100644 --- a/ppb_vector/vector2.py +++ b/ppb_vector/vector2.py @@ -92,9 +92,9 @@ def convert(cls: typing.Type[VectorOrSub], value: VectorLike) -> VectorOrSub: @property def length(self) -> float: - if not hasattr(self, '_length'): - self._length = hypot(self.x, self.y) - return self._length + # Surprisingly, caching this value provides no descernable performance + # benefit, according to microbenchmarks. + return hypot(self.x, self.y) def __len__(self: VectorOrSub) -> int: return 2