Skip to content

Commit

Permalink
Subtle misc. refactors and lint suggestions
Browse files Browse the repository at this point in the history
  • Loading branch information
mwtoews committed Dec 18, 2024
1 parent ea09e59 commit c10882d
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 71 deletions.
97 changes: 41 additions & 56 deletions affine/__init__.py
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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"
Expand All @@ -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__
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -154,7 +154,7 @@ def __new__(
h: float = 0.0,
i: float = 1.0,
):
"""Create a new object
"""Create a new object.
Parameters
----------
Expand All @@ -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
Expand Down Expand Up @@ -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().
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
23 changes: 15 additions & 8 deletions affine/tests/test_rotation.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import math

import pytest

from affine import Affine


Expand All @@ -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():
Expand All @@ -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


Expand All @@ -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)
4 changes: 2 additions & 2 deletions affine/tests/test_transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
3 changes: 1 addition & 2 deletions docs/src/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("../.."))
Expand Down Expand Up @@ -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 = ""
14 changes: 11 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

0 comments on commit c10882d

Please sign in to comment.