Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Complete type hints #121

Merged
merged 4 commits into from
Jan 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ CHANGES
3.0a1 (TBD)
-----------

- Type hint annotations for functions and methods are complete (#121).
- Affine raises ValueError if initialized with values for g, h, and i that are
not 0.0, 0.0, and 1.0, respectively (#117).
- Python version support was changed to 3.9+ (#110).
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ classifiers = [
"Programming Language :: Python :: 3 :: Only",
"Topic :: Multimedia :: Graphics :: Graphics Conversion",
"Topic :: Scientific/Engineering :: GIS",
"Typing :: Typed",
]
license = {text = "BSD-3-Clause"}
requires-python = ">=3.9"
Expand Down
91 changes: 31 additions & 60 deletions src/affine.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@
# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#############################################################################

from __future__ import annotations

from collections.abc import MutableSequence, Sequence
from functools import cached_property
import math
from typing import Optional
import warnings

from attrs import astuple, define, field

Expand All @@ -59,7 +60,7 @@ class UndefinedRotationError(AffineError):
"""The rotation angle could not be computed for this transform."""


def cos_sin_deg(deg: float):
def cos_sin_deg(deg: float) -> tuple[float, float]:
"""Return the cosine and sin for the given angle in degrees.

With special-case handling of multiples of 90 for perfect right
Expand All @@ -83,19 +84,13 @@ class Affine:
Parameters
----------
a, b, c, d, e, f, [g, h, i] : float
Coefficients of the 3 x 3 augmented affine transformation matrix.

| x' | | a b c | | x |
| y' | = | d e f | | y |
| 1 | | 0 0 1 | | 1 |

`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.
Coefficients of the 3 x 3 augmented affine transformation
matrix.
Copy link
Member Author

@sgillies sgillies Jan 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not related to type hinting: I removed a duplicated diagram.


Attributes
----------
a, b, c, d, e, f, g, h, i : float
The coefficients of the 3 x 3 augmented affine transformation
Coefficients of the 3 x 3 augmented affine transformation
matrix::

| x' | | a b c | | x |
Expand Down Expand Up @@ -159,7 +154,9 @@ def _check_i(self, attribute, value):
raise ValueError("i must be equal to 1.0")

@classmethod
def from_gdal(cls, c: float, a: float, b: float, f: float, d: float, e: float):
def from_gdal(
cls, c: float, a: float, b: float, f: float, d: float, e: float
) -> Affine:
"""Use same coefficient order as GDAL's GetGeoTransform().

Parameters
Expand All @@ -174,7 +171,7 @@ def from_gdal(cls, c: float, a: float, b: float, f: float, d: float, e: float):
return cls(a, b, c, d, e, f)

@classmethod
def identity(cls):
def identity(cls) -> Affine:
"""Return the identity transform.

Returns
Expand All @@ -184,7 +181,7 @@ def identity(cls):
return identity

@classmethod
def translation(cls, xoff: float, yoff: float):
def translation(cls, xoff: float, yoff: float) -> Affine:
"""Create a translation transform from an offset vector.

Parameters
Expand All @@ -199,7 +196,7 @@ def translation(cls, xoff: float, yoff: float):
return cls(1.0, 0.0, xoff, 0.0, 1.0, yoff)

@classmethod
def scale(cls, *scaling):
def scale(cls, *scaling: float) -> Affine:
"""Create a scaling transform from a scalar or vector.

Parameters
Expand All @@ -214,13 +211,14 @@ def scale(cls, *scaling):
Affine
"""
if len(scaling) == 1:
sx = sy = float(scaling[0])
sx = scaling[0]
sy = sx
else:
sx, sy = scaling
return cls(sx, 0.0, 0.0, 0.0, sy, 0.0)

@classmethod
def shear(cls, x_angle: float = 0, y_angle: float = 0):
def shear(cls, x_angle: float = 0.0, y_angle: float = 0.0) -> Affine:
"""Create a shear transform along one or both axes.

Parameters
Expand All @@ -237,7 +235,7 @@ def shear(cls, x_angle: float = 0, y_angle: float = 0):
return cls(1.0, mx, 0.0, my, 1.0, 0.0)

@classmethod
def rotation(cls, angle: float, pivot=None):
def rotation(cls, angle: float, pivot: Sequence[float] | None = None) -> Affine:
"""Create a rotation transform at the specified angle.

Parameters
Expand All @@ -264,7 +262,7 @@ def rotation(cls, angle: float, pivot=None):
# fmt: on

@classmethod
def permutation(cls, *scaling):
def permutation(cls, *scaling: float) -> Affine:
"""Create the permutation transform.

For 2x2 matrices, there is only one permutation matrix that is
Expand All @@ -281,7 +279,7 @@ def permutation(cls, *scaling):
"""
return cls(0.0, 1.0, 0.0, 1.0, 0.0, 0.0)

def __array__(self, dtype=None, copy=None):
def __array__(self, dtype=None, copy: bool | None = None):
"""Get affine matrix as a 3x3 NumPy array.

Parameters
Expand Down Expand Up @@ -322,7 +320,7 @@ def __repr__(self) -> str:
f" {self.d!r}, {self.e!r}, {self.f!r})"
)

def to_gdal(self):
def to_gdal(self) -> tuple[float, float, float, float, float, float]:
"""Return same coefficient order expected by GDAL's SetGeoTransform().

Returns
Expand All @@ -332,7 +330,7 @@ def to_gdal(self):
"""
return (self.c, self.a, self.b, self.f, self.d, self.e)

def to_shapely(self):
def to_shapely(self) -> tuple[float, float, float, float, float, float]:
"""Return affine transformation parameters for shapely's affinity module.

Returns
Expand Down Expand Up @@ -366,7 +364,7 @@ def determinant(self) -> float:
return self.a * self.e - self.b * self.d

@property
def _scaling(self):
def _scaling(self) -> tuple[float, float]:
"""The absolute scaling factors of the transformation.

This tuple represents the absolute value of the scaling factors of the
Expand Down Expand Up @@ -493,7 +491,9 @@ def is_proper(self) -> bool:
return self.determinant > 0.0

@property
def column_vectors(self):
def column_vectors(
self,
) -> tuple[tuple[float, float], tuple[float, float], tuple[float, float]]:
"""The values of the transform as three 2D column vectors.

Returns
Expand All @@ -503,7 +503,7 @@ def column_vectors(self):
"""
return (self.a, self.d), (self.b, self.e), (self.c, self.f)

def almost_equals(self, other, precision: Optional[float] = None) -> bool:
def almost_equals(self, other: Affine, precision: float | None = None) -> bool:
"""Compare transforms for approximate equality.

Parameters
Expand All @@ -523,7 +523,7 @@ def almost_equals(self, other, precision: Optional[float] = None) -> bool:
return all(abs(sv - ov) < precision for sv, ov in zip(self, other))

@cached_property
def _astuple(self):
def _astuple(self) -> tuple[float]:
return astuple(self)

def __getitem__(self, index):
Expand Down Expand Up @@ -582,41 +582,12 @@ def __mul__(self, other):
return NotImplemented

def __rmul__(self, other):
"""Right hand multiplication.

.. deprecated:: 2.3.0
Right multiplication will be prohibited in version 3.0. This method
will raise AffineError.

Parameters
----------
other : Affine or iterable of (vx, vy)

Returns
-------
tuple of two floats

Notes
-----
We should not be called if other is an affine instance This is
just a guarantee, since we would potentially return the wrong
answer in that case.
"""
warnings.warn(
"Right multiplication will be prohibited in version 3.0",
DeprecationWarning,
stacklevel=2,
)
assert not isinstance(other, Affine)
return self.__mul__(other)
return NotImplemented
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also not related to type hinting: removal of the right multiplication implementation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also note: I'm not providing type hints for dunder methods. These are specified by the Python language.


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

def itransform(self, seq) -> None:
def itransform(self, seq: MutableSequence[Sequence[float]]) -> None:
"""Transform a sequence of points or vectors in-place.

Parameters
Expand Down Expand Up @@ -674,7 +645,7 @@ def __getnewargs__(self):
# Miscellaneous utilities


def loadsw(s: str):
def loadsw(s: str) -> Affine:
"""Return Affine from the contents of a world file string.

This method also translates the coefficients from center- to
Expand All @@ -699,7 +670,7 @@ def loadsw(s: str):
return center * Affine.translation(-0.5, -0.5)


def dumpsw(obj) -> str:
def dumpsw(obj: Affine) -> str:
"""Return string for a world file.

This method also translates the coefficients from corner- to
Expand Down
16 changes: 8 additions & 8 deletions tests/test_transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,23 +477,23 @@ def test_shapely():
assert t.to_shapely() == (425.0, 0.0, 0.0, -425, -237481.5, 237536.4)


def test_imul_number():
t = Affine(1, 2, 3, 4, 5, 6)
def test_imul_not_implemented():
t = Affine.identity()
with pytest.raises(TypeError):
t *= 2.0


def test_rmul_notimplemented():
t = Affine.identity()
with pytest.raises(TypeError):
(1.0, 1.0) * t


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


def test_rmul_tuple():
with pytest.warns(DeprecationWarning):
t = Affine(1, 2, 3, 4, 5, 6)
(2.0, 2.0) * t


def test_associative():
point = (12, 5)
trans = Affine.translation(-10.0, -5.0)
Expand Down
Loading