diff --git a/affine/__init__.py b/affine/__init__.py index ff2be5e..e69d9ef 100644 --- a/affine/__init__.py +++ b/affine/__init__.py @@ -1,4 +1,4 @@ -"""Affine transformation matrices +"""Affine transformation matrices. The Affine package is derived from Casey Duncan's Planar package. See the copyright statement below. @@ -32,10 +32,9 @@ # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ############################################################################# -from collections import namedtuple import math import warnings - +from collections import namedtuple __all__ = ["Affine"] __author__ = "Sean Gillies" @@ -49,17 +48,18 @@ class AffineError(Exception): class TransformNotInvertibleError(AffineError): - """The transform could not be inverted""" + """The transform could not be inverted.""" class UndefinedRotationError(AffineError): - """The rotation angle could not be computed for this transform""" + """The rotation angle could not be computed for this transform.""" def cached_property(func): - """Special property decorator that caches the computed - property value in the object's instance dict the first - time it is accessed. + """Cached property decorator. + + This special property decorator caches the computed property value in the + object's instance dict the first time it is accessed. """ name = func.__name__ doc = func.__doc__ @@ -107,7 +107,7 @@ class Affine(namedtuple("Affine", ("a", "b", "c", "d", "e", "f", "g", "h", "i")) `a`, `b`, and `c` are the elements of the first row of the matrix. `d`, `e`, and `f` are the elements of the second row. - Attributes + Attributes: ---------- a, b, c, d, e, f, g, h, i : float The coefficients of the 3x3 augmented affine transformation @@ -154,7 +154,7 @@ def __new__( h: float = 0.0, i: float = 1.0, ): - """Create a new object + """Create a new object. Parameters ---------- @@ -163,17 +163,7 @@ def __new__( """ return tuple.__new__( cls, - ( - a * 1.0, - b * 1.0, - c * 1.0, - d * 1.0, - e * 1.0, - f * 1.0, - g * 1.0, - h * 1.0, - i * 1.0, - ), + tuple(map(float, (a, b, c, d, e, f, g, h, i))), ) @classmethod @@ -255,42 +245,39 @@ def rotation(cls, angle: float, pivot=None): return tuple.__new__(cls, (ca, -sa, 0.0, sa, ca, 0.0, 0.0, 0.0, 1.0)) else: px, py = pivot + c = px - px * ca + py * sa + f = py - px * sa - py * ca return tuple.__new__( cls, - ( - ca, - -sa, - px - px * ca + py * sa, - sa, - ca, - py - px * sa - py * ca, - 0.0, - 0.0, - 1.0, - ), + (ca, -sa, c, sa, ca, f, 0.0, 0.0, 1.0), ) @classmethod def permutation(cls, *scaling): - """Create the permutation transform + """Create the permutation transform. For 2x2 matrices, there is only one permutation matrix that is not the identity. :rtype: Affine """ - return tuple.__new__(cls, (0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0)) def __str__(self) -> str: """Concise string representation.""" return ( - "|% .2f,% .2f,% .2f|\n" "|% .2f,% .2f,% .2f|\n" "|% .2f,% .2f,% .2f|" - ) % self + "|{: .2f},{: .2f},{: .2f}|\n" + "|{: .2f},{: .2f},{: .2f}|\n" + "|{: .2f},{: .2f},{: .2f}|" + ).format(*self) def __repr__(self) -> str: """Precise string representation.""" - return ("Affine(%r, %r, %r,\n" " %r, %r, %r)") % self[:6] + # fmt: off + return ("Affine({!r}, {!r}, {!r},\n" + " {!r}, {!r}, {!r})" + ).format(*self[:6]) + # fmt: on def to_gdal(self): """Return same coefficient order as GDAL's SetGeoTransform(). @@ -300,7 +287,7 @@ def to_gdal(self): return (self.c, self.a, self.b, self.f, self.d, self.e) def to_shapely(self): - """Return an affine transformation matrix compatible with shapely + """Return an affine transformation matrix compatible with shapely. Shapely's affinity module expects an affine transformation matrix in (a,b,d,e,xoff,yoff) order. @@ -311,12 +298,12 @@ def to_shapely(self): @property def xoff(self) -> float: - """Alias for 'c'""" + """Alias for 'c'.""" return self.c @property def yoff(self) -> float: - """Alias for 'f'""" + """Alias for 'f'.""" return self.f @cached_property @@ -342,14 +329,14 @@ def _scaling(self): # of the matrix times its transpose, M M* # Computing trace and determinant of M M* trace = a**2 + b**2 + d**2 + e**2 - det = (a * e - b * d) ** 2 + det2 = (a * e - b * d) ** 2 - delta = trace**2 / 4 - det + delta = trace**2 / 4.0 - det2 if delta < 1e-12: - delta = 0 - - l1 = math.sqrt(trace / 2 + math.sqrt(delta)) - l2 = math.sqrt(trace / 2 - math.sqrt(delta)) + delta = 0.0 + sqrt_delta = math.sqrt(delta) + l1 = math.sqrt(trace / 2.0 + sqrt_delta) + l2 = math.sqrt(trace / 2.0 - sqrt_delta) return l1, l2 @property @@ -379,15 +366,13 @@ def rotation_angle(self) -> float: if self.is_proper or self.is_degenerate: l1, _ = self._scaling y, x = c / l1, a / l1 - return math.atan2(y, x) * 180 / math.pi + return math.degrees(math.atan2(y, x)) else: raise UndefinedRotationError @property def is_identity(self) -> bool: - """True if this transform equals the identity matrix, - within rounding limits. - """ + """True if this transform equals the identity matrix, within rounding limits.""" return self is identity or self.almost_equals(identity, self.precision) @property @@ -449,7 +434,7 @@ def is_proper(self) -> bool: @property def column_vectors(self): - """The values of the transform as three 2D column vectors""" + """The values of the transform as three 2D column vectors.""" a, b, c, d, e, f, _, _, _ = self return (a, d), (b, e), (c, f) @@ -481,7 +466,7 @@ def __add__(self, other): __iadd__ = __add__ def __mul__(self, other): - """Multiplication + """Multiplication. Apply the transform using matrix multiplication, creating a resulting object of the same type. A transform may be applied @@ -517,13 +502,13 @@ def __mul__(self, other): return NotImplemented def __rmul__(self, other): - """Right hand multiplication + """Right hand multiplication. .. deprecated:: 2.3.0 Right multiplication will be prohibited in version 3.0. This method will raise AffineError. - Notes + Notes: ----- We should not be called if other is an affine instance This is just a guarantee, since we would potentially return the wrong @@ -577,9 +562,9 @@ def __invert__(self): __hash__ = tuple.__hash__ # hash is not inherited in Py 3 def __getnewargs__(self): - """Pickle protocol support + """Pickle protocol support. - Notes + Notes: ----- Normal unpickling creates a situation where __new__ receives all 9 elements rather than the 6 that are required for the diff --git a/affine/tests/test_rotation.py b/affine/tests/test_rotation.py index 852179c..fce3676 100644 --- a/affine/tests/test_rotation.py +++ b/affine/tests/test_rotation.py @@ -1,5 +1,7 @@ import math +import pytest + from affine import Affine @@ -23,8 +25,9 @@ def test_rotation_angle(): 0---------- """ x, y = Affine.rotation(45.0) * (1.0, 0.0) - assert round(x, 14) == round(math.sqrt(2.0) / 2.0, 14) - assert round(y, 14) == round(math.sqrt(2.0) / 2.0, 14) + sqrt2div2 = math.sqrt(2.0) / 2.0 + assert x == pytest.approx(sqrt2div2) + assert y == pytest.approx(sqrt2div2) def test_rotation_matrix(): @@ -34,12 +37,16 @@ def test_rotation_matrix(): | sin(a) cos(a) | """ - rot = Affine.rotation(90.0) - assert round(rot.a, 15) == round(math.cos(math.pi / 2.0), 15) - assert round(rot.b, 15) == round(-math.sin(math.pi / 2.0), 15) + deg = 90.0 + rot = Affine.rotation(deg) + rad = math.radians(deg) + cosrad = math.cos(rad) + sinrad = math.sin(rad) + assert rot.a == pytest.approx(cosrad) + assert rot.b == pytest.approx(-sinrad) assert rot.c == 0.0 - assert round(rot.d, 15) == round(math.sin(math.pi / 2.0), 15) - assert round(rot.e, 15) == round(math.cos(math.pi / 2.0), 15) + assert rot.d == pytest.approx(sinrad) + assert rot.e == pytest.approx(cosrad) assert rot.f == 0.0 @@ -52,4 +59,4 @@ def test_rotation_matrix_pivot(): * Affine.translation(-1.0, -1.0) ) for r, e in zip(rot, exp): - assert round(r, 15) == round(e, 15) + assert r == pytest.approx(e) diff --git a/affine/tests/test_transform.py b/affine/tests/test_transform.py index b4aa3ad..a7b12ed 100644 --- a/affine/tests/test_transform.py +++ b/affine/tests/test_transform.py @@ -35,7 +35,7 @@ import pytest import affine -from affine import Affine, EPSILON +from affine import EPSILON, Affine def seq_almost_equal(t1, t2, error=0.00001): @@ -369,7 +369,7 @@ def test_bad_type_world(self): def test_bad_value_world(self): """Wrong number of parameters.""" - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="Expected 6 coefficients"): affine.loadsw("1.0\n0.0\n0.0\n1.0\n0.0\n0.0\n0.0") def test_simple_world(self): diff --git a/docs/src/conf.py b/docs/src/conf.py index 19f3841..70af83d 100644 --- a/docs/src/conf.py +++ b/docs/src/conf.py @@ -3,7 +3,6 @@ # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html import os - import sys sys.path.append(os.path.abspath("../..")) @@ -46,5 +45,5 @@ html_static_path = ["_static"] -# If this is not None, a ‘Last updated on:’ timestamp is inserted at every page bottom. +# If this is not None, a 'Last updated on:' timestamp is inserted at every page bottom. html_last_updated_fmt = "" diff --git a/pyproject.toml b/pyproject.toml index b5a4c74..be75bb8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,14 +43,22 @@ include = [ [tool.ruff.lint] select = [ - "D1", # pydocstyle - "E", # pycodestyle + "B", # flake8-bugbear + "D", # pydocstyle + "E", "W", # pycodestyle "F", # Pyflakes + "I", # isort + "PT", # flake8-pytest-style + "RUF", # Ruff-specific rules + "UP", # pyupgrade ] ignore = [ "D105", # Missing docstring in magic method ] [tool.ruff.lint.per-file-ignores] -"affine/tests/**.py" = ["D"] +"affine/tests/**.py" = ["B", "D"] "docs/**.py" = ["D"] + +[tool.ruff.lint.pydocstyle] +convention = "google"