From 44b7560fe58261d25540f5108aef5bea5b8b1d97 Mon Sep 17 00:00:00 2001 From: justvanrossum Date: Sat, 27 Feb 2021 19:54:53 +0100 Subject: [PATCH 1/2] move Vector to its own submodule, and rewrite as a tuple subclass --- Lib/fontTools/misc/arrayTools.py | 112 +++---------------------- Lib/fontTools/misc/vector.py | 139 +++++++++++++++++++++++++++++++ Lib/fontTools/varLib/__init__.py | 2 +- Tests/misc/arrayTools_test.py | 13 +-- Tests/misc/vector_test.py | 66 +++++++++++++++ 5 files changed, 217 insertions(+), 115 deletions(-) create mode 100644 Lib/fontTools/misc/vector.py create mode 100644 Tests/misc/vector_test.py diff --git a/Lib/fontTools/misc/arrayTools.py b/Lib/fontTools/misc/arrayTools.py index 138ad8f36..4b5f08298 100644 --- a/Lib/fontTools/misc/arrayTools.py +++ b/Lib/fontTools/misc/arrayTools.py @@ -4,9 +4,10 @@ so on. from fontTools.misc.py23 import * from fontTools.misc.fixedTools import otRound -from numbers import Number +from fontTools.misc.vector import Vector as _Vector import math -import operator +import warnings + def calcBounds(array): """Calculate the bounding rectangle of a 2D points array. @@ -261,107 +262,14 @@ def intRect(rect): return (xMin, yMin, xMax, yMax) -class Vector(object): - """A math-like vector. +class Vector(_Vector): - Represents an n-dimensional numeric vector. ``Vector`` objects support - vector addition and subtraction, scalar multiplication and division, - negation, rounding, and comparison tests. - - Attributes: - values: Sequence of values stored in the vector. - """ - - def __init__(self, values, keep=False): - """Initialize a vector. If ``keep`` is true, values will be copied.""" - self.values = values if keep else list(values) - - def __getitem__(self, index): - return self.values[index] - - def __len__(self): - return len(self.values) - - def __repr__(self): - return "Vector(%s)" % self.values - - def _vectorOp(self, other, op): - if isinstance(other, Vector): - assert len(self.values) == len(other.values) - a = self.values - b = other.values - return [op(a[i], b[i]) for i in range(len(self.values))] - if isinstance(other, Number): - return [op(v, other) for v in self.values] - raise NotImplementedError - - def _scalarOp(self, other, op): - if isinstance(other, Number): - return [op(v, other) for v in self.values] - raise NotImplementedError - - def _unaryOp(self, op): - return [op(v) for v in self.values] - - def __add__(self, other): - return Vector(self._vectorOp(other, operator.add), keep=True) - def __iadd__(self, other): - self.values = self._vectorOp(other, operator.add) - return self - __radd__ = __add__ - - def __sub__(self, other): - return Vector(self._vectorOp(other, operator.sub), keep=True) - def __isub__(self, other): - self.values = self._vectorOp(other, operator.sub) - return self - def __rsub__(self, other): - return other + (-self) - - def __mul__(self, other): - return Vector(self._scalarOp(other, operator.mul), keep=True) - def __imul__(self, other): - self.values = self._scalarOp(other, operator.mul) - return self - __rmul__ = __mul__ - - def __truediv__(self, other): - return Vector(self._scalarOp(other, operator.truediv), keep=True) - def __itruediv__(self, other): - self.values = self._scalarOp(other, operator.truediv) - return self - - def __pos__(self): - return Vector(self._unaryOp(operator.pos), keep=True) - def __neg__(self): - return Vector(self._unaryOp(operator.neg), keep=True) - def __round__(self): - return Vector(self._unaryOp(round), keep=True) - def toInt(self): - """Synonym for ``round``.""" - return self.__round__() - - def __eq__(self, other): - if type(other) == Vector: - return self.values == other.values - else: - return self.values == other - def __ne__(self, other): - return not self.__eq__(other) - - def __bool__(self): - return any(self.values) - __nonzero__ = __bool__ - - def __abs__(self): - return math.sqrt(sum([x*x for x in self.values])) - def dot(self, other): - """Performs vector dot product, returning sum of - ``a[0] * b[0], a[1] * b[1], ...``""" - a = self.values - b = other.values if type(other) == Vector else b - assert len(a) == len(b) - return sum([a[i] * b[i] for i in range(len(a))]) + def __init__(self, *args, **kwargs): + warnings.warn( + "fontTools.misc.arrayTools.Vector has been deprecated, please use " + "fontTools.misc.vector.Vector instead.", + DeprecationWarning, + ) def pairwise(iterable, reverse=False): diff --git a/Lib/fontTools/misc/vector.py b/Lib/fontTools/misc/vector.py new file mode 100644 index 000000000..6d47a984b --- /dev/null +++ b/Lib/fontTools/misc/vector.py @@ -0,0 +1,139 @@ +from numbers import Number +import math +import operator +import warnings + + +class Vector(tuple): + + """A math-like vector. + + Represents an n-dimensional numeric vector. ``Vector`` objects support + vector addition and subtraction, scalar multiplication and division, + negation, rounding, and comparison tests. + """ + + def __new__(cls, values, keep=False): + """Initialize a vector..""" + if keep is not False: + warnings.warn( + "the 'keep' argument has been deprecated", + DeprecationWarning, + ) + if type(values) == Vector: + # No need to create a new object + return values + return super().__new__(cls, values) + + def __repr__(self): + return f"{self.__class__.__name__}({super().__repr__()})" + + def _vectorOp(self, other, op): + if isinstance(other, Vector): + assert len(self) == len(other) + return self.__class__(op(a, b) for a, b in zip(self, other)) + if isinstance(other, Number): + return self.__class__(op(v, other) for v in self) + raise NotImplementedError() + + def _scalarOp(self, other, op): + if isinstance(other, Number): + return self.__class__(op(v, other) for v in self) + raise NotImplementedError() + + def _unaryOp(self, op): + return self.__class__(op(v) for v in self) + + def __add__(self, other): + return self._vectorOp(other, operator.add) + + __radd__ = __add__ + + def __sub__(self, other): + return self._vectorOp(other, operator.sub) + + def __rsub__(self, other): + return self._vectorOp(other, _operator_rsub) + + def __mul__(self, other): + return self._scalarOp(other, operator.mul) + + __rmul__ = __mul__ + + def __truediv__(self, other): + return self._scalarOp(other, operator.truediv) + + def __rtruediv__(self, other): + return self._scalarOp(other, _operator_rtruediv) + + def __pos__(self): + return self._unaryOp(operator.pos) + + def __neg__(self): + return self._unaryOp(operator.neg) + + def __round__(self): + return self._unaryOp(round) + + def __eq__(self, other): + if isinstance(other, list): + # bw compat Vector([1, 2, 3]) == [1, 2, 3] + other = tuple(other) + return super().__eq__(other) + + def __ne__(self, other): + return not self.__eq__(other) + + def __bool__(self): + return any(self) + + __nonzero__ = __bool__ + + def __abs__(self): + return math.sqrt(sum(x * x for x in self)) + + def length(self): + """Return the length of the vector. Equivalent to abs(vector).""" + return abs(self) + + def normalized(self): + """Return the normalized vector of the vector.""" + return self / abs(self) + + def dot(self, other): + """Performs vector dot product, returning the sum of + ``a[0] * b[0], a[1] * b[1], ...``""" + assert len(self) == len(other) + return sum(a * b for a, b in zip(self, other)) + + # Deprecated methods/properties + + def toInt(self): + warnings.warn( + "the 'toInt' method has been deprecated, use round(vector) instead", + DeprecationWarning, + ) + return self.__round__() + + @property + def values(self): + warnings.warn( + "the 'values' attribute has been deprecated, use " + "the vector object itself instead", + DeprecationWarning, + ) + return list(self) + + @values.setter + def values(self, values): + raise AttributeError( + "can't set attribute, the 'values' attribute has been deprecated", + ) + + +def _operator_rsub(a, b): + return operator.sub(b, a) + + +def _operator_rtruediv(a, b): + return operator.truediv(b, a) diff --git a/Lib/fontTools/varLib/__init__.py b/Lib/fontTools/varLib/__init__.py index 605fda2a7..dd320b040 100644 --- a/Lib/fontTools/varLib/__init__.py +++ b/Lib/fontTools/varLib/__init__.py @@ -20,7 +20,7 @@ API *will* change in near future. """ from fontTools.misc.py23 import * from fontTools.misc.fixedTools import otRound -from fontTools.misc.arrayTools import Vector +from fontTools.misc.vector import Vector from fontTools.ttLib import TTFont, newTable from fontTools.ttLib.tables._f_v_a_r import Axis, NamedInstance from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates diff --git a/Tests/misc/arrayTools_test.py b/Tests/misc/arrayTools_test.py index 73e0ab17e..127f153c8 100644 --- a/Tests/misc/arrayTools_test.py +++ b/Tests/misc/arrayTools_test.py @@ -1,7 +1,7 @@ from fontTools.misc.py23 import * from fontTools.misc.py23 import round3 from fontTools.misc.arrayTools import ( - Vector, calcBounds, calcIntBounds, updateBounds, pointInRect, pointsInRect, + calcBounds, calcIntBounds, updateBounds, pointInRect, pointsInRect, vectorLength, asInt16, normRect, scaleRect, offsetRect, insetRect, sectRect, unionRect, rectCenter, intRect) import math @@ -88,14 +88,3 @@ def test_rectCenter(): def test_intRect(): assert intRect((0.9, 2.9, 3.1, 4.1)) == (0, 2, 4, 5) - - -def test_Vector(): - v = Vector([100, 200]) - assert v == Vector([100, 200]) - assert v == [100, 200] - assert v + Vector([1, 2]) == [101, 202] - assert v - Vector([1, 2]) == [99, 198] - assert v * 2 == [200, 400] - assert v * 0.5 == [50, 100] - assert v / 2 == [50, 100] diff --git a/Tests/misc/vector_test.py b/Tests/misc/vector_test.py new file mode 100644 index 000000000..7448cef1d --- /dev/null +++ b/Tests/misc/vector_test.py @@ -0,0 +1,66 @@ +import math +import pytest +from fontTools.misc.arrayTools import Vector as ArrayVector +from fontTools.misc.vector import Vector + + +def test_Vector(): + v = Vector((100, 200)) + assert repr(v) == "Vector((100, 200))" + assert v == Vector((100, 200)) + assert v == Vector([100, 200]) + assert v == (100, 200) + assert (100, 200) == v + assert v == [100, 200] + assert [100, 200] == v + assert v is Vector(v) + assert v + 10 == (110, 210) + assert 10 + v == (110, 210) + assert v + Vector((1, 2)) == (101, 202) + assert v - Vector((1, 2)) == (99, 198) + assert v * 2 == (200, 400) + assert 2 * v == (200, 400) + assert v * 0.5 == (50, 100) + assert v / 2 == (50, 100) + assert 2 / v == (0.02, 0.01) + v = Vector((3, 4)) + assert abs(v) == 5 # length + assert v.length() == 5 + assert v.normalized() == Vector((0.6, 0.8)) + assert abs(Vector((1, 1, 1))) == math.sqrt(3) + assert bool(Vector((0, 0, 1))) + assert not bool(Vector((0, 0, 0))) + v1 = Vector((2, 3)) + v2 = Vector((3, 4)) + assert v1.dot(v2) == 18 + v = Vector((2, 4)) + assert round(v / 3) == (1, 1) + + +def test_deprecated(): + with pytest.warns( + DeprecationWarning, + match="fontTools.misc.arrayTools.Vector has been deprecated", + ): + ArrayVector((1, 2)) + with pytest.warns( + DeprecationWarning, + match="the 'keep' argument has been deprecated", + ): + Vector((1, 2), keep=True) + v = Vector((1, 2)) + with pytest.warns( + DeprecationWarning, + match="the 'toInt' method has been deprecated", + ): + v.toInt() + with pytest.warns( + DeprecationWarning, + match="the 'values' attribute has been deprecated", + ): + v.values + with pytest.raises( + AttributeError, + match="the 'values' attribute has been deprecated", + ): + v.values = [12, 23] From 9c9ab5ac3ab88ef9465923c81f9c314118a71858 Mon Sep 17 00:00:00 2001 From: justvanrossum Date: Sat, 27 Feb 2021 20:05:53 +0100 Subject: [PATCH 2/2] removed info-less doc string --- Lib/fontTools/misc/vector.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/fontTools/misc/vector.py b/Lib/fontTools/misc/vector.py index 6d47a984b..995385c04 100644 --- a/Lib/fontTools/misc/vector.py +++ b/Lib/fontTools/misc/vector.py @@ -14,7 +14,6 @@ class Vector(tuple): """ def __new__(cls, values, keep=False): - """Initialize a vector..""" if keep is not False: warnings.warn( "the 'keep' argument has been deprecated",