Skip to content

Commit

Permalink
Misc: more lints, tests and minor fixes (#119)
Browse files Browse the repository at this point in the history
  • Loading branch information
mwtoews authored Jan 25, 2025
1 parent 57c1220 commit 61c31c6
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 62 deletions.
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Matrices describing 2D affine transformation of the plane.
:alt: Documentation Status

The Affine package is derived from Casey Duncan's Planar package. Please see
the copyright statement in `affine.py <affine.py>`__.
the copyright statement in `src/affine.py <src/affine.py>`__.

Usage
-----
Expand Down
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ include = [
"tests/",
]

[tool.pytest.ini_options]
pythonpath = "src"
testpaths = ["tests"]

[tool.ruff.lint]
select = [
"B", # flake8-bugbear
Expand All @@ -56,7 +60,9 @@ select = [
"I", # isort
"NPY", # NumPy-specific rules
"PT", # flake8-pytest-style
"RET", # flake8-return
"RUF", # Ruff-specific rules
"SIM", # flake8-simplify
"UP", # pyupgrade
]
ignore = [
Expand Down
71 changes: 27 additions & 44 deletions src/affine.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ def cos_sin_deg(deg: float):
deg = deg % 360.0
if deg == 90.0:
return 0.0, 1.0
elif deg == 180.0:
if deg == 180.0:
return -1.0, 0
elif deg == 270.0:
if deg == 270.0:
return 0, -1.0
rad = math.radians(deg)
return math.cos(rad), math.sin(rad)
Expand Down Expand Up @@ -255,18 +255,13 @@ def rotation(cls, angle: float, pivot=None):
ca, sa = cos_sin_deg(angle)
if pivot is None:
return cls(ca, -sa, 0.0, sa, ca, 0.0)
else:
px, py = pivot
# fmt: off
return cls(
ca,
-sa,
px - px * ca + py * sa,
sa,
ca,
py - px * sa - py * ca,
)
# fmt: on
px, py = pivot
# fmt: off
return cls(
ca, -sa, px - px * ca + py * sa,
sa, ca, py - px * sa - py * ca,
)
# fmt: on

@classmethod
def permutation(cls, *scaling):
Expand Down Expand Up @@ -426,8 +421,7 @@ def rotation_angle(self) -> float:
l1, _ = self._scaling
y, x = self.d / l1, self.a / l1
return math.degrees(math.atan2(y, x))
else:
raise UndefinedRotationError
raise UndefinedRotationError

@property
def is_identity(self) -> bool:
Expand Down Expand Up @@ -570,16 +564,9 @@ def __mul__(self, other):
-------
Affine or a tuple of two floats
"""
sa, sb, sc, sd, se, sf = self.a, self.b, self.c, self.d, self.e, self.f
sa, sb, sc, sd, se, sf = self[:6]
if isinstance(other, Affine):
oa, ob, oc, od, oe, of = (
other.a,
other.b,
other.c,
other.d,
other.e,
other.f,
)
oa, ob, oc, od, oe, of = other[:6]
return self.__class__(
sa * oa + sb * od,
sa * ob + sb * oe,
Expand All @@ -588,12 +575,11 @@ def __mul__(self, other):
sd * ob + se * oe,
sd * oc + se * of + sf,
)
else:
try:
vx, vy = other
return (vx * sa + vy * sb + sc, vx * sd + vy * se + sf)
except (ValueError, TypeError):
return NotImplemented
try:
vx, vy = other
return (vx * sa + vy * sb + sc, vx * sd + vy * se + sf)
except (ValueError, TypeError):
return NotImplemented

def __rmul__(self, other):
"""Right hand multiplication.
Expand All @@ -608,7 +594,7 @@ def __rmul__(self, other):
Returns
-------
Affine
tuple of two floats
Notes
-----
Expand All @@ -626,10 +612,9 @@ def __rmul__(self, other):

def __imul__(self, other):
"""Provide wrapper for `__mul__`, however `other` is not modified in-place."""
if isinstance(other, Affine) or isinstance(other, tuple):
if isinstance(other, (Affine, tuple)):
return self.__mul__(other)
else:
return NotImplemented
return NotImplemented

def itransform(self, seq) -> None:
"""Transform a sequence of points or vectors in-place.
Expand All @@ -644,7 +629,7 @@ def itransform(self, seq) -> None:
The input sequence is mutated in-place.
"""
if self is not identity and self != identity:
sa, sb, sc, sd, se, sf = self.a, self.b, self.c, self.d, self.e, self.f
sa, sb, sc, sd, se, sf = self[:6]
for i, (x, y) in enumerate(seq):
seq[i] = (x * sa + y * sb + sc, x * sd + y * se + sf)

Expand All @@ -659,19 +644,17 @@ def __invert__(self):
if self.is_degenerate:
raise TransformNotInvertibleError("Cannot invert degenerate transform")
idet = 1.0 / self.determinant
sa, sb, sc, sd, se, sf = self.a, self.b, self.c, self.d, self.e, self.f
sa, sb, sc, sd, se, sf = self[:6]
ra = se * idet
rb = -sb * idet
rd = -sd * idet
re = sa * idet
# fmt: off
return self.__class__(
ra,
rb,
-sc * ra - sf * rb,
rd,
re,
-sc * rd - sf * re,
ra, rb, -sc * ra - sf * rb,
rd, re, -sc * rd - sf * re,
)
# fmt: on

def __getnewargs__(self):
"""Pickle protocol support.
Expand All @@ -682,7 +665,7 @@ def __getnewargs__(self):
9 elements rather than the 6 that are required for the
constructor. This method ensures that only the 6 are provided.
"""
return self.a, self.b, self.c, self.d, self.e, self.f
return self[:6]


identity = Affine(1, 0, 0, 0, 1, 0)
Expand Down
52 changes: 39 additions & 13 deletions tests/test_numpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,57 @@


def test_array():
tfm = Affine(*np.linspace(0.1, 0.6, 6))
tfm_ar = np.array(tfm)
a, b, c, d, e, f = (np.arange(6) + 1) / 10
tfm = Affine(a, b, c, d, e, f)
expected = np.array(
[
[0.1, 0.2, 0.3],
[0.4, 0.5, 0.6],
[0.0, 0.0, 1.0],
[a, b, c],
[d, e, f],
[0, 0, 1],
],
)
assert tfm_ar.shape == (3, 3)
assert tfm_ar.dtype == np.float64
testing.assert_allclose(tfm_ar, expected)
ar = np.array(tfm)
assert ar.shape == (3, 3)
assert ar.dtype == np.float64
testing.assert_array_equal(ar, expected)

# dtype option
tfm_ar = np.array(tfm, dtype=np.float32)
assert tfm_ar.shape == (3, 3)
assert tfm_ar.dtype == np.float32
testing.assert_allclose(tfm_ar, expected)
ar = np.array(tfm, dtype=np.float32)
assert ar.shape == (3, 3)
assert ar.dtype == np.float32
testing.assert_allclose(ar, expected)

# copy option
tfm_ar = np.array(tfm, copy=True) # default None does the same
ar = np.array(tfm, copy=True) # default None does the same
testing.assert_allclose(ar, expected)

# Behaviour of copy=False is different between NumPy 1.x and 2.x
if int(np.version.short_version.split(".", 1)[0]) >= 2:
with pytest.raises(ValueError, match="A copy is always created"):
np.array(tfm, copy=False)
else:
testing.assert_allclose(np.array(tfm, copy=False), expected)


def test_linalg():
# cross-check properties with numpy's linear algebra module
ar = np.array(
[
[0, -2, 2],
[3, 0, 5],
[0, 0, 1],
]
)
tfm = Affine(*ar.flatten())
assert tfm.determinant == pytest.approx(6.0)
assert np.linalg.det(ar) == pytest.approx(6.0)

expected_inv = np.array(
[
[0, 1 / 3, -5 / 3],
[-1 / 2, 0, 1],
[0, 0, 1],
]
)
testing.assert_allclose(~tfm, expected_inv)
testing.assert_allclose(np.linalg.inv(ar), expected_inv)
22 changes: 18 additions & 4 deletions tests/test_transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,22 @@ def test_slice_last_row():
assert t[-3:] == (0, 0, 1)


def test_members():
t = Affine(1, 2, 3, 4, 5, 6)
assert t.a == 1
assert t.b == 2
assert t.c == 3
assert t.d == 4
assert t.e == 5
assert t.f == 6
assert t.g == 0
assert t.h == 0
assert t.i == 1
# these are aliases
assert t.c is t.xoff
assert t.f is t.yoff


def test_members_are_floats():
t = Affine(1, 2, 3, 4, 5, 6)
for m in t:
Expand Down Expand Up @@ -463,15 +479,13 @@ def test_shapely():

def test_imul_number():
t = Affine(1, 2, 3, 4, 5, 6)
try:
with pytest.raises(TypeError):
t *= 2.0
except TypeError:
assert True


def test_mul_tuple():
t = Affine(1, 2, 3, 4, 5, 6)
t * (2.0, 2.0)
assert t * (2, 2) == (9, 24)


def test_rmul_tuple():
Expand Down

0 comments on commit 61c31c6

Please sign in to comment.