From 1d0a5622a3fa151876be18f888287f1e32405fdf Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Fri, 27 Oct 2023 18:47:35 +0100 Subject: [PATCH 01/41] develop 1.2 --- docs/HISTORY.txt | 7 ++++++- pygeoif/about.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/HISTORY.txt b/docs/HISTORY.txt index 87776683..29c6ae33 100644 --- a/docs/HISTORY.txt +++ b/docs/HISTORY.txt @@ -1,7 +1,12 @@ Changelog ========= -1.1.1 (unreleased) +1.2.0 (unreleased) +------------------ + + - remove Python 3.7 support + +1.1.1 (2023/10/27) ------------------ - Use pyproject.toml instead of setup.py diff --git a/pygeoif/about.py b/pygeoif/about.py index b3df8abd..10c86489 100644 --- a/pygeoif/about.py +++ b/pygeoif/about.py @@ -3,4 +3,4 @@ The only purpose of this module is to provide a version number for the package. """ -__version__ = "1.1.1" +__version__ = "1.2.0" From 5ab65bbcb19733725e660ff00dce32fece22c5f7 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 28 Oct 2023 14:06:15 +0100 Subject: [PATCH 02/41] first iteration to make geometries immutable --- pygeoif/geometry.py | 100 +++++++++++++++++++++++--------------- tests/test_linear_ring.py | 24 --------- 2 files changed, 61 insertions(+), 63 deletions(-) diff --git a/pygeoif/geometry.py b/pygeoif/geometry.py index 6ea62edd..be96a039 100644 --- a/pygeoif/geometry.py +++ b/pygeoif/geometry.py @@ -20,6 +20,7 @@ import math import warnings from itertools import chain +from typing import Any from typing import Iterable from typing import Iterator from typing import NoReturn @@ -49,6 +50,18 @@ class _Geometry: """Base Class for geometry objects.""" + __slots__ = ("_geoms",) + + def __setattr__(self, *args: Any) -> NoReturn: + raise AttributeError( + f"Attributes of {self.__class__.__name__} cannot be changed", + ) + + def __delattr__(self, *args: Any) -> NoReturn: + raise AttributeError( + f"Attributes of {self.__class__.__name__} cannot be deleted", + ) + def __str__(self) -> str: return self.wkt @@ -226,18 +239,22 @@ def __init__(self, x: float, y: float, z: Optional[float] = None) -> None: 2 or 3 coordinate parameters: x, y, [z] : float Easting, northing, and elevation. """ - self._coordinates = cast( - PointType, - tuple( - coordinate - for coordinate in (x, y, z) - if coordinate is not None and not math.isnan(coordinate) + object.__setattr__( + self, + "_geoms", + cast( + PointType, + tuple( + coordinate + for coordinate in (x, y, z) + if coordinate is not None and not math.isnan(coordinate) + ), ), ) def __repr__(self) -> str: """Return the representation.""" - return f"{self.geom_type}{self._coordinates}" + return f"{self.geom_type}{self._geoms}" @property def is_empty(self) -> bool: @@ -246,50 +263,50 @@ def is_empty(self) -> bool: A Point is considered empty when it has less than 2 coordinates. """ - return len(self._coordinates) < 2 # noqa: PLR2004 + return len(self._geoms) < 2 # noqa: PLR2004 @property def x(self) -> float: """Return x coordinate.""" - return self._coordinates[0] + return self._geoms[0] @property def y(self) -> float: """Return y coordinate.""" - return self._coordinates[1] + return self._geoms[1] @property def z(self) -> Optional[float]: """Return z coordinate.""" if self.has_z: - return self._coordinates[2] # type: ignore [misc] + return self._geoms[2] # type: ignore [misc] msg = f"The {self!r} geometry does not have z values" raise DimensionError(msg) @property def coords(self) -> Tuple[PointType]: """Return the geometry coordinates.""" - return (self._coordinates,) + return (self._geoms,) @property def has_z(self) -> bool: """Return True if the geometry's coordinate sequence(s) have z values.""" - return len(self._coordinates) == 3 # noqa: PLR2004 + return len(self._geoms) == 3 # noqa: PLR2004 @property def _wkt_coords(self) -> str: - return " ".join(str(coordinate) for coordinate in self._coordinates) + return " ".join(str(coordinate) for coordinate in self._geoms) @property def _wkt_inset(self) -> str: """Return Z for 3 dimensional geometry or an empty string for 2 dimensions.""" - return " Z " if len(self._coordinates) == 3 else " " # noqa: PLR2004 + return " Z " if len(self._geoms) == 3 else " " # noqa: PLR2004 @property def __geo_interface__(self) -> GeoInterface: """Return the geo interface.""" geo_interface = super().__geo_interface__ - geo_interface["coordinates"] = cast(PointType, tuple(self._coordinates)) + geo_interface["coordinates"] = cast(PointType, tuple(self._geoms)) return geo_interface @classmethod @@ -337,7 +354,7 @@ def __init__(self, coordinates: LineType) -> None: >>> a = LineString([(0, 0), (1, 0), (1, 1)]) """ - self._geoms = self._set_geoms(coordinates) + object.__setattr__(self, "_geoms", self._set_geoms(coordinates)) def __repr__(self) -> str: """Return the representation.""" @@ -422,7 +439,7 @@ def _set_geoms(coordinates: LineType) -> Tuple[Point, ...]: ) last_len = len(coord) point = Point(*coord) - if not point.is_empty: + if point: geoms.append(point) return tuple(geoms) @@ -461,7 +478,7 @@ def __init__(self, coordinates: LineType) -> None: """ super().__init__(coordinates) if not self.is_empty and self._geoms[0].coords != self._geoms[-1].coords: - self._geoms = (*self._geoms, self._geoms[0]) + object.__setattr__(self, "_geoms", (*self._geoms, self._geoms[0])) @property def centroid(self) -> Optional[Point]: @@ -544,10 +561,11 @@ def __init__( >>> coords = ((0., 0.), (0., 1.), (1., 1.), (1., 0.), (0., 0.)) >>> polygon = Polygon(coords) """ - self._interiors: Tuple[LinearRing, ...] = () + interiors: Tuple[LinearRing, ...] = () if holes: - self._interiors = tuple(LinearRing(hole) for hole in holes) - self._exterior = LinearRing(shell) + interiors = tuple(LinearRing(hole) for hole in holes) + exterior = LinearRing(shell) + object.__setattr__(self, "_geoms", (exterior, interiors)) def __repr__(self) -> str: """Return the representation.""" @@ -556,12 +574,12 @@ def __repr__(self) -> str: @property def exterior(self) -> LinearRing: """Return the exterior Linear Ring of the polygon.""" - return self._exterior + return self._geoms[0] @property def interiors(self) -> Iterator[LinearRing]: """Interiors (Holes) of the polygon.""" - yield from (interior for interior in self._interiors if interior) + yield from (interior for interior in self._geoms[1] if interior) @property def is_empty(self) -> bool: @@ -570,7 +588,7 @@ def is_empty(self) -> bool: A polygon is empty when it does not have an exterior. """ - return self._exterior.is_empty + return self._geoms[0].is_empty @property def coords(self) -> PolygonType: @@ -579,7 +597,7 @@ def coords(self) -> PolygonType: Note that this is not implemented in Shapely. """ - if self._interiors: + if self._geoms[1]: return self.exterior.coords, tuple( interior.coords for interior in self.interiors if interior ) @@ -588,7 +606,7 @@ def coords(self) -> PolygonType: @property def has_z(self) -> Optional[bool]: """Return True if the geometry's coordinate sequence(s) have z values.""" - return self._exterior.has_z + return self._geoms[0].has_z @property def maybe_valid(self) -> bool: @@ -602,7 +620,7 @@ def maybe_valid(self) -> bool: return False return ( all(interior.maybe_valid for interior in self.interiors) - if self._exterior.maybe_valid + if self.exterior.maybe_valid else False ) @@ -759,7 +777,7 @@ def __init__(self, points: Sequence[PointType], unique: bool = False) -> None: """ if unique: points = set(points) # type: ignore [assignment] - self._geoms = tuple(Point(*point) for point in points) + object.__setattr__(self, "_geoms", tuple(Point(*point) for point in points)) def __len__(self) -> int: """Return the number of points in this MultiPoint.""" @@ -831,7 +849,7 @@ def __init__(self, lines: Sequence[LineType], unique: bool = False) -> None: """ if unique: lines = {tuple(line) for line in lines} # type: ignore [assignment] - self._geoms = tuple(LineString(line) for line in lines) + object.__setattr__(self, "_geoms", tuple(LineString(line) for line in lines)) def __len__(self) -> int: """Return the number of lines in the collection.""" @@ -926,14 +944,18 @@ def __init__(self, polygons: Sequence[PolygonType], unique: bool = False) -> Non if unique: polygons = set(polygons) # type: ignore [assignment] - self._geoms = tuple( - Polygon( - shell=polygon[0], - holes=polygon[1] # type: ignore [misc] - if len(polygon) == 2 # noqa: PLR2004 - else None, - ) - for polygon in polygons + object.__setattr__( + self, + "_geoms", + tuple( + Polygon( + shell=polygon[0], + holes=polygon[1] # type: ignore [misc] + if len(polygon) == 2 # noqa: PLR2004 + else None, + ) + for polygon in polygons + ), ) def __len__(self) -> int: @@ -1036,7 +1058,7 @@ def __init__( ---- geometries (Iterable[Geometry] """ - self._geoms = tuple(geom for geom in geometries if geom) + object.__setattr__(self, "_geoms", tuple(geom for geom in geometries if geom)) def __eq__(self, other: object) -> bool: """ diff --git a/tests/test_linear_ring.py b/tests/test_linear_ring.py index 529d705c..8e4d3c9b 100644 --- a/tests/test_linear_ring.py +++ b/tests/test_linear_ring.py @@ -230,30 +230,6 @@ def test_centroid_valid() -> None: assert line.centroid == geometry.Point(2, 1) -def test_centroid_invalid() -> None: - ring = geometry.LinearRing([(0, 0), (2, 0), (2, 2), (0, 2)]) - line = geometry.LineString( - [ - (28, 16), - (37, 31), - (21, 50), - (-21, 64), - (-84, 64), - (-148, 46), - (-95, 10), - (-72, 46), - (-40, 64), - (-9, 64), - (12, 50), - (20, 31), - (15, 16), - ], - ) - ring._geoms = line._geoms - - assert ring.centroid is None - - def test_empty() -> None: ring = geometry.LinearRing([]) From 80513bdb438ce27dea22ace2ae9f74cd6b6779c9 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 28 Oct 2023 14:36:30 +0100 Subject: [PATCH 03/41] Make geometries hashable --- pygeoif/geometry.py | 42 ++++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/pygeoif/geometry.py b/pygeoif/geometry.py index be96a039..60fda545 100644 --- a/pygeoif/geometry.py +++ b/pygeoif/geometry.py @@ -21,6 +21,7 @@ import warnings from itertools import chain from typing import Any +from typing import Hashable from typing import Iterable from typing import Iterator from typing import NoReturn @@ -52,16 +53,23 @@ class _Geometry: __slots__ = ("_geoms",) - def __setattr__(self, *args: Any) -> NoReturn: + _geoms: Hashable + + def __setattr__(self, *args: Any) -> NoReturn: # noqa: ANN401 + msg = f"Attributes of {self.__class__.__name__} cannot be changed" raise AttributeError( - f"Attributes of {self.__class__.__name__} cannot be changed", + msg, ) - def __delattr__(self, *args: Any) -> NoReturn: + def __delattr__(self, *args: Any) -> NoReturn: # noqa: ANN401 + msg = f"Attributes of {self.__class__.__name__} cannot be deleted" raise AttributeError( - f"Attributes of {self.__class__.__name__} cannot be deleted", + msg, ) + def __hash__(self) -> int: + return hash(self._geoms) + def __str__(self) -> str: return self.wkt @@ -230,6 +238,8 @@ class Point(_Geometry): 1.0 """ + _geoms: PointType + def __init__(self, x: float, y: float, z: Optional[float] = None) -> None: """ Initialize a Point. @@ -339,6 +349,8 @@ class LineString(_Geometry): A sequence of Points """ + _geoms: Tuple[Point, ...] + def __init__(self, coordinates: LineType) -> None: """ Initialize a Linestring. @@ -538,6 +550,8 @@ class Polygon(_Geometry): A sequence of rings which bound all existing holes. """ + _geoms: Tuple[LinearRing, ...] + def __init__( self, shell: LineType, @@ -579,7 +593,11 @@ def exterior(self) -> LinearRing: @property def interiors(self) -> Iterator[LinearRing]: """Interiors (Holes) of the polygon.""" - yield from (interior for interior in self._geoms[1] if interior) + yield from ( + interior + for interior in self._geoms[1] # type: ignore [attr-defined] + if interior + ) @property def is_empty(self) -> bool: @@ -710,11 +728,7 @@ def coords(self) -> NoReturn: @property def has_z(self) -> Optional[bool]: """Return True if any geometry of the collection have z values.""" - return ( - any(geom.has_z for geom in self.geoms) - if self._geoms # type: ignore [attr-defined] - else None - ) + return any(geom.has_z for geom in self.geoms) if self._geoms else None @property def geoms(self) -> Iterator[_Geometry]: @@ -753,6 +767,8 @@ class MultiPoint(_MultiGeometry): A sequence of Points """ + _geoms: Tuple[Point, ...] + def __init__(self, points: Sequence[PointType], unique: bool = False) -> None: """ Create a collection of one or more points. @@ -829,6 +845,8 @@ class MultiLineString(_MultiGeometry): A sequence of LineStrings """ + _geoms: Tuple[LineString, ...] + def __init__(self, lines: Sequence[LineType], unique: bool = False) -> None: """ Initialize the MultiLineString. @@ -911,6 +929,8 @@ class MultiPolygon(_MultiGeometry): A sequence of `Polygon` instances """ + _geoms: Tuple[Polygon, ...] + def __init__(self, polygons: Sequence[PolygonType], unique: bool = False) -> None: """ Initialize a Multipolygon. @@ -1047,6 +1067,8 @@ class isn't generally supported by ordinary GIS sw (viewers and so on). So {'type': 'Point', 'coordinates': (1.0, -1.0)}]} """ + _geoms: Tuple[Union[Geometry, "GeometryCollection"], ...] + def __init__( self, geometries: Iterable[Union[Geometry, "GeometryCollection"]], From 26769f57b6e229a8008baf59a6aff20167aa3802 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 28 Oct 2023 15:53:12 +0100 Subject: [PATCH 04/41] add tests for hash, get and set attribute --- tests/test_base.py | 74 ++++++++++++++++++++++++++++++++ tests/test_geometrycollection.py | 28 ++++++++++++ tests/test_line.py | 6 +++ tests/test_linear_ring.py | 6 +++ tests/test_multiline.py | 8 ++++ tests/test_multipoint.py | 6 +++ tests/test_multipolygon.py | 38 ++++++++++++++++ tests/test_point.py | 12 ++++++ tests/test_polygon.py | 6 +++ 9 files changed, 184 insertions(+) diff --git a/tests/test_base.py b/tests/test_base.py index 615a56e0..0585f40e 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -72,3 +72,77 @@ def test_get_bounds() -> None: base_geo = geometry._Geometry() with pytest.raises(NotImplementedError, match="^Must be implemented by subclass$"): assert base_geo._get_bounds() + + +@pytest.mark.parametrize( + ("attr_val", "expected_error", "expected_error_message"), + [ + # Happy path tests + ( + ("attribute", "value"), + AttributeError, + "Attributes of _Geometry cannot be changed", + ), + ( + ("another_attribute", 123), + AttributeError, + "Attributes of _Geometry cannot be changed", + ), + ( + ("yet_another_attribute", [1, 2, 3]), + AttributeError, + "Attributes of _Geometry cannot be changed", + ), + # Edge cases + (("", "value"), AttributeError, "Attributes of _Geometry cannot be changed"), + ((None, "value"), TypeError, "attribute name must be string, not 'NoneType'"), + # Error cases + ((123, "value"), TypeError, "attribute name must be string, not 'int'"), + (([1, 2, 3], "value"), TypeError, "attribute name must be string, not 'list'"), + ], +) +def test_setattr(attr_val, expected_error, expected_error_message) -> None: + base_geo = geometry._Geometry() + + with pytest.raises(expected_error, match=f"^{expected_error_message}$"): + setattr(base_geo, *attr_val) + + +@pytest.mark.parametrize( + ("attr", "expected_error", "expected_error_message"), + [ + ( + "attr1", + AttributeError, + "Attributes of _Geometry cannot be deleted", + ), # realistic test value + ( + "", + AttributeError, + "Attributes of _Geometry cannot be deleted", + ), # edge case: empty string + ( + None, + TypeError, + "attribute name must be string, not 'NoneType'", + ), # edge case: None + ( + 123, + TypeError, + "attribute name must be string, not 'int'", + ), # error case: non-string attribute + ], + ids=[ + "realistic_test_value", + "edge_case_empty_string", + "edge_case_None", + "error_case_non_string_attribute", + ], +) +def test_delattr(attr, expected_error, expected_error_message) -> None: + # Arrange + base_geo = geometry._Geometry() + + # Act + with pytest.raises(expected_error, match=f"^{expected_error_message}$"): + delattr(base_geo, attr) diff --git a/tests/test_geometrycollection.py b/tests/test_geometrycollection.py index 79696c9b..a0bccc78 100644 --- a/tests/test_geometrycollection.py +++ b/tests/test_geometrycollection.py @@ -1,5 +1,7 @@ """Test Baseclass.""" +import pytest + from pygeoif import geometry @@ -425,3 +427,29 @@ def test_nested_geometry_collection_repr_eval() -> None: ).__geo_interface__ == gc.__geo_interface__ ) + + +@pytest.mark.xfail(reason="GeometryCollection is not hashable") +def test_nested_geometry_collection_hash() -> None: + multipoint = geometry.MultiPoint([(0, 0), (1, 1), (1, 2), (2, 2)]) + gc1 = geometry.GeometryCollection([geometry.Point(0, 0), multipoint]) + line1 = geometry.LineString([(0, 0), (3, 1)]) + gc2 = geometry.GeometryCollection([gc1, line1]) + poly1 = geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]) + e = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] + i = [(1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)] + poly2 = geometry.Polygon(e, [i]) + p0 = geometry.Point(0, 0) + p1 = geometry.Point(-1, -1) + ring = geometry.LinearRing([(0, 0), (1, 1), (1, 0), (0, 0)]) + line = geometry.LineString([(0, 0), (1, 1)]) + gc = geometry.GeometryCollection([gc2, poly1, poly2, p0, p1, ring, line]) + + assert hash(gc) == 0 + + +@pytest.mark.xfail(reason="GeometryCollection is not hashable") +def test_hash_empty() -> None: + gc = geometry.GeometryCollection([]) + + assert hash(gc) == hash(geometry.GeometryCollection([])) diff --git a/tests/test_line.py b/tests/test_line.py index b0b4df8a..2d6fd5bf 100644 --- a/tests/test_line.py +++ b/tests/test_line.py @@ -19,6 +19,12 @@ def test_coords_get_3d() -> None: assert line.coords == ((0.0, 0.0, 0), (1.0, 1.0, 1)) +def test_hash() -> None: + line = geometry.LineString([(0, 0, 0), (1, 1, 1)]) + + assert hash(line) == hash(geometry.LineString([(0, 0, 0), (1, 1, 1)])) + + def test_set_geoms_raises() -> None: line = geometry.LineString([(0, 0), (1, 0)]) # pragma: no mutate diff --git a/tests/test_linear_ring.py b/tests/test_linear_ring.py index 8e4d3c9b..3a67a879 100644 --- a/tests/test_linear_ring.py +++ b/tests/test_linear_ring.py @@ -240,3 +240,9 @@ def test_empty_bounds() -> None: ring = geometry.LinearRing([]) assert ring.bounds == () + + +def test_hash() -> None: + ring = geometry.LinearRing([(0, 0), (4, 0), (4, 2), (0, 2)]) + + assert hash(ring) == hash(((0, 0), (4, 0), (4, 2), (0, 2), (0, 0))) diff --git a/tests/test_multiline.py b/tests/test_multiline.py index 05a3b3cd..ff189e2b 100644 --- a/tests/test_multiline.py +++ b/tests/test_multiline.py @@ -200,3 +200,11 @@ def test_empty_bounds() -> None: lines = geometry.MultiLineString([]) assert lines.bounds == () + + +def test_hash() -> None: + lines = geometry.MultiLineString(([(0, 0), (1, 1)], [[0.0, 0.0], [1.0, 2.0]])) + + assert hash(lines) == hash( + geometry.MultiLineString(([(0, 0), (1, 1)], [[0.0, 0.0], [1.0, 2.0]])), + ) diff --git a/tests/test_multipoint.py b/tests/test_multipoint.py index d4af0030..ba9da565 100644 --- a/tests/test_multipoint.py +++ b/tests/test_multipoint.py @@ -148,6 +148,12 @@ def test_from_points_unique() -> None: ) +def test_hash() -> None: + multipoint = geometry.MultiPoint([(0, 0), (1, 0), (2, 2)]) + + assert hash(multipoint) == hash(geometry.MultiPoint([(0, 0), (1, 0), (2, 2)])) + + def test_empty() -> None: multipoint = geometry.MultiPoint([(1, None)]) diff --git a/tests/test_multipolygon.py b/tests/test_multipolygon.py index d4bb9f1e..bd8538ef 100644 --- a/tests/test_multipolygon.py +++ b/tests/test_multipolygon.py @@ -284,3 +284,41 @@ def test_empty_bounds() -> None: polys = geometry.MultiPolygon([]) assert polys.bounds == () + + +def test_hash() -> None: + polys = geometry.MultiPolygon( + ( + ( + ((0, 0), (0, 2), (2, 2), (2, 0), (0, 0)), + ( + ((1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)), + ((1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)), + ), + ), + (((0, 0, 0), (1, 1, 0), (1, 0, 0), (0, 0, 0)),), + ( + ((0, 0), (0, 2), (2, 2), (2, 0), (0, 0)), + (((1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)),), + ), + ), + ) + + assert hash(polys) == hash( + geometry.MultiPolygon( + ( + ( + ((0, 0), (0, 2), (2, 2), (2, 0), (0, 0)), + ( + ((1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)), + ((1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)), + ), + ), + (((0, 0, 0), (1, 1, 0), (1, 0, 0), (0, 0, 0)),), + ( + ((0, 0), (0, 2), (2, 2), (2, 0), (0, 0)), + (((1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)),), + ), + ), + ), + ) diff --git a/tests/test_point.py b/tests/test_point.py index 33a3fb93..37ad8803 100644 --- a/tests/test_point.py +++ b/tests/test_point.py @@ -57,6 +57,12 @@ def test_xy() -> None: assert point.y == 0 +def test_hash() -> None: + point = geometry.Point(1.0, 2.0, 3.0) + + assert hash(point) == hash((1.0, 2.0, 3.0)) + + def test_xy_raises_error_accessing_z() -> None: point = geometry.Point(1, 0) @@ -238,3 +244,9 @@ def test_empty_bounds() -> None: point = geometry.Point(None, None) assert point.bounds == () + + +def test_hash_empty() -> None: + point = geometry.Point(None, None) + + assert hash(point) == hash(()) diff --git a/tests/test_polygon.py b/tests/test_polygon.py index 4ef462d3..07c8b056 100644 --- a/tests/test_polygon.py +++ b/tests/test_polygon.py @@ -20,6 +20,12 @@ def test_coords_with_holes() -> None: ) +def test_hash() -> None: + polygon = geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]) + + assert hash(polygon) == hash(geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)])) + + def test_geo_interface_shell_only() -> None: polygon = geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]) From e5e9e0469b18599ca5d288d760e6c77136356a78 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 28 Oct 2023 16:09:29 +0100 Subject: [PATCH 05/41] catch pypy differing error messages --- tests/test_base.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_base.py b/tests/test_base.py index 0585f40e..862c7f79 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -95,10 +95,10 @@ def test_get_bounds() -> None: ), # Edge cases (("", "value"), AttributeError, "Attributes of _Geometry cannot be changed"), - ((None, "value"), TypeError, "attribute name must be string, not 'NoneType'"), + ((None, "value"), TypeError, ".*attribute name must be string.*"), # Error cases - ((123, "value"), TypeError, "attribute name must be string, not 'int'"), - (([1, 2, 3], "value"), TypeError, "attribute name must be string, not 'list'"), + ((123, "value"), TypeError, ".*attribute name must be string.*"), + (([1, 2, 3], "value"), TypeError, ".*attribute name must be string.*"), ], ) def test_setattr(attr_val, expected_error, expected_error_message) -> None: @@ -124,12 +124,12 @@ def test_setattr(attr_val, expected_error, expected_error_message) -> None: ( None, TypeError, - "attribute name must be string, not 'NoneType'", + ".*attribute name must be string.*", ), # edge case: None ( 123, TypeError, - "attribute name must be string, not 'int'", + ".*attribute name must be string.*", ), # error case: non-string attribute ], ids=[ From 4ba9aab34a172e740fcfebfe27ef4a3db9616677 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 28 Oct 2023 16:21:11 +0100 Subject: [PATCH 06/41] small refactor --- pygeoif/geometry.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pygeoif/geometry.py b/pygeoif/geometry.py index 60fda545..ee259e05 100644 --- a/pygeoif/geometry.py +++ b/pygeoif/geometry.py @@ -575,9 +575,7 @@ def __init__( >>> coords = ((0., 0.), (0., 1.), (1., 1.), (1., 0.), (0., 0.)) >>> polygon = Polygon(coords) """ - interiors: Tuple[LinearRing, ...] = () - if holes: - interiors = tuple(LinearRing(hole) for hole in holes) + interiors = tuple(LinearRing(hole) for hole in holes) if holes else () exterior = LinearRing(shell) object.__setattr__(self, "_geoms", (exterior, interiors)) From 0b98057046441b46ff4d30b6d7728f70f4747f95 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 28 Oct 2023 20:18:09 +0100 Subject: [PATCH 07/41] Make geometry collections hashable --- docs/HISTORY.txt | 1 + pygeoif/geometry.py | 4 ++++ tests/test_geometrycollection.py | 18 +++++++++++++----- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/docs/HISTORY.txt b/docs/HISTORY.txt index 29c6ae33..b347f272 100644 --- a/docs/HISTORY.txt +++ b/docs/HISTORY.txt @@ -5,6 +5,7 @@ Changelog ------------------ - remove Python 3.7 support + - Geometries are now immutable and hashable 1.1.1 (2023/10/27) ------------------ diff --git a/pygeoif/geometry.py b/pygeoif/geometry.py index ee259e05..8ea35cc3 100644 --- a/pygeoif/geometry.py +++ b/pygeoif/geometry.py @@ -1110,6 +1110,10 @@ def __eq__(self, other: object) -> bool: second=other.__geo_interface__, # type: ignore [attr-defined] ) + def __hash__(self) -> int: + """Return the hash of the collection.""" + return hash(self.wkt) + def __len__(self) -> int: """ Length of the collection. diff --git a/tests/test_geometrycollection.py b/tests/test_geometrycollection.py index a0bccc78..27915bf0 100644 --- a/tests/test_geometrycollection.py +++ b/tests/test_geometrycollection.py @@ -1,7 +1,5 @@ """Test Baseclass.""" -import pytest - from pygeoif import geometry @@ -429,7 +427,6 @@ def test_nested_geometry_collection_repr_eval() -> None: ) -@pytest.mark.xfail(reason="GeometryCollection is not hashable") def test_nested_geometry_collection_hash() -> None: multipoint = geometry.MultiPoint([(0, 0), (1, 1), (1, 2), (2, 2)]) gc1 = geometry.GeometryCollection([geometry.Point(0, 0), multipoint]) @@ -445,10 +442,21 @@ def test_nested_geometry_collection_hash() -> None: line = geometry.LineString([(0, 0), (1, 1)]) gc = geometry.GeometryCollection([gc2, poly1, poly2, p0, p1, ring, line]) - assert hash(gc) == 0 + assert hash(gc) == hash( + geometry.GeometryCollection( + [ + gc2, + poly1, + poly2, + p0, + p1, + ring, + line, + ], + ), + ) -@pytest.mark.xfail(reason="GeometryCollection is not hashable") def test_hash_empty() -> None: gc = geometry.GeometryCollection([]) From f6d9679870e693baaf04782ec77f98ac68a11ff2 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 28 Oct 2023 21:58:59 +0100 Subject: [PATCH 08/41] fix black badge --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 551ed028..79f7586c 100644 --- a/README.rst +++ b/README.rst @@ -39,7 +39,7 @@ It was written to provide clean and python only geometries for fastkml_ .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black -.. image:: https://img.shields.io/badge/type-checker-mypy-blue +.. image:: https://img.shields.io/badge/type%20checker-mypy-blue :target: http://mypy-lang.org/ .. image:: https://www.openhub.net/p/pygeoif/widgets/project_thin_badge.gif From 0049e81978532263c78deb61ec8c5929efc88448 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 28 Oct 2023 22:15:02 +0100 Subject: [PATCH 09/41] add alt to badges --- README.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 79f7586c..e00f8988 100644 --- a/README.rst +++ b/README.rst @@ -32,18 +32,23 @@ It was written to provide clean and python only geometries for fastkml_ .. image:: https://github.com/cleder/pygeoif/actions/workflows/run-all-tests.yml/badge.svg?branch=main :target: https://github.com/cleder/pygeoif/actions/workflows/run-all-tests.yml + :alt: GitHub Actions .. image:: https://codecov.io/gh/cleder/pygeoif/branch/main/graph/badge.svg?token=2EfiwBXs9X :target: https://codecov.io/gh/cleder/pygeoif + :alt: Codecov .. image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://github.com/psf/black + :target: https://github.com/psf/ + :alt: Black .. image:: https://img.shields.io/badge/type%20checker-mypy-blue :target: http://mypy-lang.org/ + :alt: Mypy .. image:: https://www.openhub.net/p/pygeoif/widgets/project_thin_badge.gif :target: https://www.openhub.net/p/pygeoif/ + :alt: Openhub .. image:: https://www.codefactor.io/repository/github/cleder/pygeoif/badge/main :target: https://www.codefactor.io/repository/github/cleder/pygeoif/overview/main From b9c34d613b4d64e0365bab9a3ef01b441710caee Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 28 Oct 2023 23:27:01 +0100 Subject: [PATCH 10/41] add versions and implementations badges to readme --- README.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.rst b/README.rst index e00f8988..e80cd971 100644 --- a/README.rst +++ b/README.rst @@ -58,6 +58,14 @@ It was written to provide clean and python only geometries for fastkml_ :target: https://github.com/pre-commit/pre-commit :alt: pre-commit +.. image:: https://img.shields.io/pypi/pyversions/pygeoif.svg + :target: https://pypi.python.org/pypi/pygeoif/ + :alt: Supported Python versions + +.. image:: https://img.shields.io/pypi/implementation/pygeoif.svg + :target: https://pypi.python.org/pypi/pygeoif/ + :alt: Supported Python implementations + Example ======== From 5cc649d1b0adc51bc7001cdf645663afc95760b0 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sun, 29 Oct 2023 16:26:33 +0000 Subject: [PATCH 11/41] documentation initial version --- .readthedocs.yaml | 23 +++++++ README.rst | 2 + docs/{CONTRIBUTORS.txt => CONTRIBUTORS.rst} | 0 docs/{HISTORY.txt => HISTORY.rst} | 0 docs/LICENSE.rst | 21 +++++++ docs/LICENSE.txt | 19 ------ docs/Makefile | 20 ++++++ docs/conf.py | 42 +++++++++++++ docs/index.rst | 24 ++++++++ docs/make.bat | 35 +++++++++++ docs/modules.rst | 10 +++ docs/overview.rst | 5 ++ docs/pygeoif.rst | 67 +++++++++++++++++++++ docs/requirements.txt | 3 + 14 files changed, 252 insertions(+), 19 deletions(-) create mode 100644 .readthedocs.yaml rename docs/{CONTRIBUTORS.txt => CONTRIBUTORS.rst} (100%) rename docs/{HISTORY.txt => HISTORY.rst} (100%) create mode 100644 docs/LICENSE.rst delete mode 100644 docs/LICENSE.txt create mode 100644 docs/Makefile create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/make.bat create mode 100644 docs/modules.rst create mode 100644 docs/overview.rst create mode 100644 docs/pygeoif.rst create mode 100644 docs/requirements.txt diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..0a3b1864 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,23 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details +--- +# Required +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/conf.py + +# We recommend specifying your dependencies to enable reproducible builds: +# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: docs/requirements.txt +... diff --git a/README.rst b/README.rst index e80cd971..560d308d 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,8 @@ Introduction ============ +.. inclusion-marker-do-not-remove + PyGeoIf provides a `GeoJSON-like protocol `_ for geo-spatial (GIS) vector data. Other Python programs and packages that you may have heard of already diff --git a/docs/CONTRIBUTORS.txt b/docs/CONTRIBUTORS.rst similarity index 100% rename from docs/CONTRIBUTORS.txt rename to docs/CONTRIBUTORS.rst diff --git a/docs/HISTORY.txt b/docs/HISTORY.rst similarity index 100% rename from docs/HISTORY.txt rename to docs/HISTORY.rst diff --git a/docs/LICENSE.rst b/docs/LICENSE.rst new file mode 100644 index 00000000..d1fdccef --- /dev/null +++ b/docs/LICENSE.rst @@ -0,0 +1,21 @@ + +License +======= + +Copyright (C) 2012 - 2023 Christian Ledermann + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License (LGPL_) as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License (LGPL_) for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +.. _LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.0.en.html diff --git a/docs/LICENSE.txt b/docs/LICENSE.txt deleted file mode 100644 index d8d8473b..00000000 --- a/docs/LICENSE.txt +++ /dev/null @@ -1,19 +0,0 @@ - - Pygeoif is a basic implementation of the __geo_interface__ in - pure Python - - Copyright (C) 2012 - 2023 Christian Ledermann - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..d4bb2cbb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..4f6f7022 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,42 @@ +# noqa: D100, INP001 +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = "pygeoif" +copyright = "2023, Christian Ledermann" # noqa: A001 +author = "Christian Ledermann" +release = "1.2" + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + "sphinx.ext.duration", + "sphinx.ext.doctest", + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", +] + +templates_path = ["_templates"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = "alabaster" +html_static_path = ["_static"] +try: + import sphinx_rtd_theme + + html_theme = "sphinx_rtd_theme" + html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +except ImportError: + pass + +autodoc_member_order = "bysource" diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..986eb3c2 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,24 @@ +.. pygeoif documentation master file, created by + sphinx-quickstart on Sun Oct 29 13:56:59 2023. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to pygeoif's documentation! +=================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + overview + modules + HISTORY + CONTRIBUTORS + LICENSE + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 00000000..32bb2452 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/modules.rst b/docs/modules.rst new file mode 100644 index 00000000..a2c8296b --- /dev/null +++ b/docs/modules.rst @@ -0,0 +1,10 @@ +API Reference +============= + +.. toctree:: + :maxdepth: 4 + + pygeoif + +.. autosummary:: + :toctree: generated diff --git a/docs/overview.rst b/docs/overview.rst new file mode 100644 index 00000000..75741435 --- /dev/null +++ b/docs/overview.rst @@ -0,0 +1,5 @@ +Overview +========= + +.. include:: ../README.rst + :start-after: inclusion-marker-do-not-remove diff --git a/docs/pygeoif.rst b/docs/pygeoif.rst new file mode 100644 index 00000000..024b062e --- /dev/null +++ b/docs/pygeoif.rst @@ -0,0 +1,67 @@ +pygeoif +======= + + +Module contents +--------------- + + +Submodules +---------- + +pygeoif.geometry module +----------------------- + +.. automodule:: pygeoif.geometry + :members: + :member-order: bysource + :undoc-members: + :show-inheritance: + +pygeoif.feature module +---------------------- + +.. automodule:: pygeoif.feature + :members: + :undoc-members: + :show-inheritance: + +pygeoif.factories module +------------------------ + +.. automodule:: pygeoif.factories + :members: + :undoc-members: + :show-inheritance: + +pygeoif.functions module +------------------------ + +.. automodule:: pygeoif.functions + :members: + :undoc-members: + :show-inheritance: + +pygeoif.exceptions module +------------------------- + +.. automodule:: pygeoif.exceptions + :members: + :undoc-members: + :show-inheritance: + +pygeoif.types module +-------------------- + +.. automodule:: pygeoif.types + :members: + :undoc-members: + :show-inheritance: + +pygeoif.about module +-------------------- + +.. automodule:: pygeoif.about + :members: + :undoc-members: + :special-members: __version__ diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..2cd888ca --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,3 @@ +sphinx +sphinx-autodoc-typehints +sphinx-rtd-theme From 96c37da4df248de1e32fc108afb072a2aea4679e Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sun, 29 Oct 2023 16:51:11 +0000 Subject: [PATCH 12/41] add path to sphinx configuration --- docs/conf.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 4f6f7022..f14d98ff 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -3,14 +3,20 @@ # # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information +from pygeoif import about # noqa: E402 + project = "pygeoif" copyright = "2023, Christian Ledermann" # noqa: A001 author = "Christian Ledermann" -release = "1.2" +release = about.__version__ # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration From 2416729885f51f5632adc10d3a3f61b5346f1c55 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sun, 29 Oct 2023 17:07:18 +0000 Subject: [PATCH 13/41] fix conf.py --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index f14d98ff..9feba681 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -6,7 +6,7 @@ import sys from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parent)) +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information From ef64c7dc702ec83448116b052542c2bf58d71ceb Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sun, 29 Oct 2023 17:08:57 +0000 Subject: [PATCH 14/41] add typing_extensions to docs requirements --- docs/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/requirements.txt b/docs/requirements.txt index 2cd888ca..650fbfd3 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,4 @@ sphinx sphinx-autodoc-typehints sphinx-rtd-theme +typing-extensions From 12bbd0d2fc51e74e8980177e9c1b9e6e7f9ef6c0 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sun, 29 Oct 2023 17:15:18 +0000 Subject: [PATCH 15/41] add rtd link to readme and toml --- README.rst | 4 ++++ pyproject.toml | 1 + 2 files changed, 5 insertions(+) diff --git a/README.rst b/README.rst index 560d308d..66501347 100644 --- a/README.rst +++ b/README.rst @@ -36,6 +36,10 @@ It was written to provide clean and python only geometries for fastkml_ :target: https://github.com/cleder/pygeoif/actions/workflows/run-all-tests.yml :alt: GitHub Actions +.. image:: https://readthedocs.org/projects/pygeoif/badge/?version=latest + :target: https://pygeoif.readthedocs.io/en/latest/?badge=latest + :alt: Documentation Status + .. image:: https://codecov.io/gh/cleder/pygeoif/branch/main/graph/badge.svg?token=2EfiwBXs9X :target: https://codecov.io/gh/cleder/pygeoif :alt: Codecov diff --git a/pyproject.toml b/pyproject.toml index f0570285..0101bf95 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,6 +91,7 @@ content-type = "text/x-rst" file = "README.rst" [project.urls] +Documentation = "https://pygeoif.readthedocs.io/" Homepage = "https://github.com/cleder/pygeoif/" [tool.flake8] From 77305cda470029e0f509a67c4ecbd45f29970c5a Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sun, 29 Oct 2023 19:23:46 +0000 Subject: [PATCH 16/41] Improve README --- .gitignore | 1 + README.rst | 57 +++++++++++++++++++++++++++++++----------------------- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index cc9c4328..97717c04 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ html/ venv/ __pycache__/ *.stderr* +docs/_* diff --git a/README.rst b/README.rst index 66501347..64873415 100644 --- a/README.rst +++ b/README.rst @@ -3,9 +3,10 @@ Introduction .. inclusion-marker-do-not-remove -PyGeoIf provides a `GeoJSON-like protocol `_ for geo-spatial (GIS) vector data. +PyGeoIf provides a `GeoJSON-like protocol `_ +for geo-spatial (GIS) vector data. -Other Python programs and packages that you may have heard of already +Other Python programs and packages that you may have heard of that implement this protocol: * `ArcPy `_ @@ -13,6 +14,9 @@ implement this protocol: * `PySAL `_ * `Shapely `_ * `pyshp `_ +* `GeoPandas `_ +* `Karta `_ +* `mapnik `_ When you want to write your own geospatial library with support for this protocol you may use pygeoif as a starting point and build @@ -72,6 +76,14 @@ It was written to provide clean and python only geometries for fastkml_ :target: https://pypi.python.org/pypi/pygeoif/ :alt: Supported Python implementations +Installation +------------ + +You can install PyGeoIf from pypi using pip:: + + pip install pygeoif + + Example ======== @@ -321,9 +333,8 @@ Create a geometry from its WKT representation signed_area ------------ -Return the signed area enclosed by a ring using the linear time -algorithm at http://www.cgafaq.info/wiki/Polygon_Area. A value >= 0 -indicates a counter-clockwise oriented ring. +Return the signed area enclosed by a ring. +A value >= 0 indicates a counter-clockwise oriented ring. orient @@ -349,28 +360,13 @@ Return the ``__geo_interface__`` dictionary. Development =========== -Installation ------------- - -You can install PyGeoIf from pypi using pip:: - - pip install pygeoif - -Testing -------- - -Install the requirements with ``pip install -r test-requirements.txt`` -and run the unit and static tests with:: +Clone this repository, create a virtualenv with Python 3.8 or later with +``python3 -m venv .venv`` and activate it with ``source .venv/bin/activate``. - pytest pygeoif - pytest --doctest-glob="README.rst" - yesqa pygeoif/*.py - black pygeoif - flake8 pygeoif - mypy pygeoif +Then install the requirements with ``pip install -e ".[dev]"``. pre-commit -~~~~~~~~~~~ +---------- Install the ``pre-commit`` hook with:: @@ -381,6 +377,19 @@ and check the code with:: pre-commit run --all-files +Testing +------- + +Run the unit and static tests with:: + + pytest tests + pytest --doctest-glob="README.rst" + black pygeoif + ruff pygeoif + flake8 pygeoif + mypy pygeoif + + Acknowledgments ================ From 8b9592beda7460a98c79113b205a2f328434dc10 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sun, 29 Oct 2023 23:38:14 +0000 Subject: [PATCH 17/41] one WKT to rule them all --- docs/LICENSE.rst | 2 +- pygeoif/geometry.py | 19 +++++-------------- tests/test_base.py | 3 ++- tests/test_factories.py | 18 +++++++++--------- tests/test_functions.py | 2 +- tests/test_geometrycollection.py | 8 ++++---- tests/test_multiline.py | 4 ++-- tests/test_multipoint.py | 2 +- tests/test_multipolygon.py | 2 +- 9 files changed, 26 insertions(+), 34 deletions(-) diff --git a/docs/LICENSE.rst b/docs/LICENSE.rst index d1fdccef..c769a96c 100644 --- a/docs/LICENSE.rst +++ b/docs/LICENSE.rst @@ -18,4 +18,4 @@ You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -.. _LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.0.en.html +.. _LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html diff --git a/pygeoif/geometry.py b/pygeoif/geometry.py index 8ea35cc3..87704207 100644 --- a/pygeoif/geometry.py +++ b/pygeoif/geometry.py @@ -182,7 +182,11 @@ def _wkt_coords(self) -> str: @property def _wkt_inset(self) -> str: """Return Z for 3 dimensional geometry or an empty string for 2 dimensions.""" - return "" + if self.is_empty: + return f"{self._wkt_type} EMPTY" + if self.has_z: + return " Z " + return " " @property def _wkt_type(self) -> str: @@ -307,11 +311,6 @@ def has_z(self) -> bool: def _wkt_coords(self) -> str: return " ".join(str(coordinate) for coordinate in self._geoms) - @property - def _wkt_inset(self) -> str: - """Return Z for 3 dimensional geometry or an empty string for 2 dimensions.""" - return " Z " if len(self._geoms) == 3 else " " # noqa: PLR2004 - @property def __geo_interface__(self) -> GeoInterface: """Return the geo interface.""" @@ -407,10 +406,6 @@ def maybe_valid(self) -> bool: """ return len({p.coords[0] for p in self._geoms}) > 1 - @property - def _wkt_inset(self) -> str: - return self.geoms[0]._wkt_inset # noqa: SLF001 - @property def _wkt_coords(self) -> str: return ", ".join(point._wkt_coords for point in self.geoms) # noqa: SLF001 @@ -648,10 +643,6 @@ def _wkt_coords(self) -> str: ) return f"({ec}){ic}" - @property - def _wkt_inset(self) -> str: - return self.exterior._wkt_inset # noqa: SLF001 - @property def __geo_interface__(self) -> GeoInterface: """Return the geo interface.""" diff --git a/tests/test_base.py b/tests/test_base.py index 862c7f79..af98c21b 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -37,7 +37,8 @@ def test_empty() -> None: def test_wkt_inset() -> None: base_geo = geometry._Geometry() - assert base_geo._wkt_inset == "" + with pytest.raises(NotImplementedError, match="^Must be implemented by subclass$"): + assert base_geo._wkt_inset == "" def test_wkt_coordinates() -> None: diff --git a/tests/test_factories.py b/tests/test_factories.py index 1c29aaa7..96235841 100644 --- a/tests/test_factories.py +++ b/tests/test_factories.py @@ -108,8 +108,8 @@ class TestWKT: (30 20, 20 25, 20 15, 30 20)))""", """MULTIPOLYGON (((30 20, 10 40, 45 40, 30 20)), ((15 5, 40 10, 10 20, 5 10, 15 5)))""", - "MULTIPOLYGON(((0 0,10 0,10 10,0 10,0 0)),((5 5,7 5,7 7,5 7, 5 5)))", - "GEOMETRYCOLLECTION(POINT(10 10), POINT(30 30), LINESTRING(15 15, 20 20))", + "MULTIPOLYGON (((0 0,10 0,10 10,0 10,0 0)),((5 5,7 5,7 7,5 7, 5 5)))", + "GEOMETRYCOLLECTION (POINT(10 10), POINT(30 30), LINESTRING(15 15, 20 20))", ] # these are valid WKTs but not supported @@ -209,7 +209,7 @@ def test_multipoint(self) -> None: assert isinstance(p, geometry.MultiPoint) assert next(iter(p.geoms)).x == 3.5 assert list(p.geoms)[1].y == 10.5 - assert p.wkt == "MULTIPOINT(3.5 5.6, 4.8 10.5)" + assert p.wkt == "MULTIPOINT (3.5 5.6, 4.8 10.5)" p = factories.from_wkt("MULTIPOINT ((10 40), (40 30), (20 20), (30 10))") assert isinstance(p, geometry.MultiPoint) assert next(iter(p.geoms)).x == 10.0 @@ -221,14 +221,14 @@ def test_multipoint(self) -> None: def test_multilinestring(self) -> None: p = factories.from_wkt( - "MULTILINESTRING((3 4,10 50,20 25),(-5 -8,-10 -8,-15 -4))", + "MULTILINESTRING ((3 4,10 50,20 25),(-5 -8,-10 -8,-15 -4))", ) assert isinstance(p, geometry.MultiLineString) assert next(iter(p.geoms)).coords == (((3, 4), (10, 50), (20, 25))) assert list(p.geoms)[1].coords == (((-5, -8), (-10, -8), (-15, -4))) assert ( - p.wkt == "MULTILINESTRING((3 4, 10 50, " + p.wkt == "MULTILINESTRING ((3 4, 10 50, " "20 25),(-5 -8, " "-10 -8, -15 -4))" ) @@ -241,12 +241,12 @@ def test_multilinestring_1(self) -> None: assert isinstance(p, geometry.MultiLineString) assert p.wkt == ( - "MULTILINESTRING((10 10, 20 20, 10 40),(40 40, 30 30, 40 20, 30 10))" + "MULTILINESTRING ((10 10, 20 20, 10 40),(40 40, 30 30, 40 20, 30 10))" ) def test_multipolygon(self) -> None: p = factories.from_wkt( - "MULTIPOLYGON(((0 0,10 20,30 40,0 0)," + "MULTIPOLYGON (((0 0,10 20,30 40,0 0)," "(1 1,2 2,3 3,1 1))," "((100 100,110 110,120 120,100 100)))", ) @@ -273,7 +273,7 @@ def test_multipolygon(self) -> None: (100.0, 100.0), ) assert ( - p.wkt == "MULTIPOLYGON(((0 0, 10 20, " + p.wkt == "MULTIPOLYGON (((0 0, 10 20, " "30 40, 0 0)," "(1 1, 2 2, 3 3, 1 1))," "((100 100, 110 110," @@ -322,7 +322,7 @@ def test_geometrycollection(self) -> None: assert len(list(gc.geoms)) == 2 assert isinstance(next(iter(gc.geoms)), geometry.Point) assert isinstance(list(gc.geoms)[1], geometry.LineString) - assert gc.wkt == "GEOMETRYCOLLECTION(POINT (4 6), LINESTRING (4 6, 7 10))" + assert gc.wkt == "GEOMETRYCOLLECTION (POINT (4 6), LINESTRING (4 6, 7 10))" def test_wkt_ok(self) -> None: for wkt in self.wkt_ok: diff --git a/tests/test_functions.py b/tests/test_functions.py index 1872d64d..e1441a22 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -126,7 +126,7 @@ def test_signed_area_crescent_ish() -> None: def test_empty_hull() -> None: - assert not convex_hull([]) + assert convex_hull([]) == [] def test_point() -> None: diff --git a/tests/test_geometrycollection.py b/tests/test_geometrycollection.py index 27915bf0..59065667 100644 --- a/tests/test_geometrycollection.py +++ b/tests/test_geometrycollection.py @@ -70,7 +70,7 @@ def test_geo_wkt() -> None: gc = geometry.GeometryCollection([poly1, poly2, p0, p1, ring, line]) assert gc.wkt == ( - "GEOMETRYCOLLECTION" + "GEOMETRYCOLLECTION " "(POLYGON ((0 0, 1 1, 1 0, 0 0)), " "POLYGON ((0 0, 0 2, 2 2, 2 0, 0 0),(1 0, 0.5 0.5, 1 1, 1.5 0.5, 1 0)), " "POINT (0 0), POINT (-1 -1), " @@ -271,7 +271,7 @@ def test_multipoint_wkt() -> None: multipoint = geometry.MultiPoint([(0, 0), (1, 1), (1, 2), (2, 2)]) gc = geometry.GeometryCollection([multipoint]) - assert gc.wkt == "GEOMETRYCOLLECTION(MULTIPOINT(0 0, 1 1, 1 2, 2 2))" + assert gc.wkt == "GEOMETRYCOLLECTION (MULTIPOINT (0 0, 1 1, 1 2, 2 2))" def test_multipoint_repr() -> None: @@ -309,8 +309,8 @@ def test_nested_geometry_collection() -> None: gc3 = geometry.GeometryCollection([gc2, poly1]) assert gc3.wkt == ( - "GEOMETRYCOLLECTION(GEOMETRYCOLLECTION(GEOMETRYCOLLECTION(" - "POINT (0 0), MULTIPOINT(0 0, 1 1, 1 2, 2 2)), LINESTRING (0 0, 3 1)), " + "GEOMETRYCOLLECTION (GEOMETRYCOLLECTION (GEOMETRYCOLLECTION (" + "POINT (0 0), MULTIPOINT (0 0, 1 1, 1 2, 2 2)), LINESTRING (0 0, 3 1)), " "POLYGON ((0 0, 1 1, 1 0, 0 0)))" ) diff --git a/tests/test_multiline.py b/tests/test_multiline.py index ff189e2b..0aadecae 100644 --- a/tests/test_multiline.py +++ b/tests/test_multiline.py @@ -52,13 +52,13 @@ def test_wkt() -> None: ([(0, 0), (1, 1), (1, 2), (2, 2)], [[0.0, 0.0], [1.0, 2.0]]), ) - assert lines.wkt == "MULTILINESTRING((0 0, 1 1, 1 2, 2 2),(0.0 0.0, 1.0 2.0))" + assert lines.wkt == "MULTILINESTRING ((0 0, 1 1, 1 2, 2 2),(0.0 0.0, 1.0 2.0))" def test_wkt_single_line() -> None: lines = geometry.MultiLineString(([(0, 0), (1, 1), (1, 2), (2, 2)],)) - assert lines.wkt == "MULTILINESTRING((0 0, 1 1, 1 2, 2 2))" + assert lines.wkt == "MULTILINESTRING ((0 0, 1 1, 1 2, 2 2))" def test_repr() -> None: diff --git a/tests/test_multipoint.py b/tests/test_multipoint.py index ba9da565..9b64bef3 100644 --- a/tests/test_multipoint.py +++ b/tests/test_multipoint.py @@ -81,7 +81,7 @@ def test_unique() -> None: def test_wkt() -> None: multipoint = geometry.MultiPoint([(0, 0), (1, 1), (1, 2), (2, 2)]) - assert multipoint.wkt == "MULTIPOINT(0 0, 1 1, 1 2, 2 2)" + assert multipoint.wkt == "MULTIPOINT (0 0, 1 1, 1 2, 2 2)" def test_repr() -> None: diff --git a/tests/test_multipolygon.py b/tests/test_multipolygon.py index bd8538ef..4ae637ba 100644 --- a/tests/test_multipolygon.py +++ b/tests/test_multipolygon.py @@ -89,7 +89,7 @@ def test_wkt() -> None: ) assert polys.wkt == ( - "MULTIPOLYGON(((0.0 0.0, 0.0 1.0, 1.0 1.0, 1.0 0.0, 0.0 0.0)," + "MULTIPOLYGON (((0.0 0.0, 0.0 1.0, 1.0 1.0, 1.0 0.0, 0.0 0.0)," "(0.1 0.1, 0.1 0.2, 0.2 0.2, 0.2 0.1, 0.1 0.1))," "((0.0 0.0, 0.0 1.0, 1.0 1.0, 1.0 0.0, 0.0 0.0)))" ) From e3ff7ce0da36526c36a82c69ff6ec3370c99e154 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 16:35:48 +0000 Subject: [PATCH 18/41] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.10.0 → 23.10.1](https://github.com/psf/black/compare/23.10.0...23.10.1) - [github.com/astral-sh/ruff-pre-commit: v0.1.1 → v0.1.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.1...v0.1.3) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 996a8286..22ff14e3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,11 +41,11 @@ repos: hooks: - id: isort - repo: https://github.com/psf/black - rev: 23.10.0 + rev: 23.10.1 hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.1.1' + rev: 'v0.1.3' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 5afa1d5605a27415013f578a9ef9e873f52b2d9a Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Tue, 31 Oct 2023 13:00:27 +0000 Subject: [PATCH 19/41] EMPTY geometries are already handled in `geometry.wkt` --- pygeoif/geometry.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pygeoif/geometry.py b/pygeoif/geometry.py index 87704207..58ed051a 100644 --- a/pygeoif/geometry.py +++ b/pygeoif/geometry.py @@ -182,11 +182,7 @@ def _wkt_coords(self) -> str: @property def _wkt_inset(self) -> str: """Return Z for 3 dimensional geometry or an empty string for 2 dimensions.""" - if self.is_empty: - return f"{self._wkt_type} EMPTY" - if self.has_z: - return " Z " - return " " + return " Z " if self.has_z else " " @property def _wkt_type(self) -> str: From 0f8cc7f25c743b181d5ff1515140d896713e66cf Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Tue, 31 Oct 2023 19:44:41 +0000 Subject: [PATCH 20/41] refactor orient --- pygeoif/factories.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/pygeoif/factories.py b/pygeoif/factories.py index 74831167..3e0ffa38 100644 --- a/pygeoif/factories.py +++ b/pygeoif/factories.py @@ -68,19 +68,15 @@ def orient(polygon: Polygon, ccw: bool = True) -> Polygon: # noqa: FBT001, FBT0 and the interiors will be in clockwise orientation, or the other way round when ccw is False. """ + + def get_oriented_ring(ring: LineType) -> LineType: + return ring if signed_area(ring) / s >= 0 else ring[::-1] + s = 1.0 if ccw else -1.0 - rings = [] - ring = polygon.exterior - if signed_area(ring.coords) / s >= 0: - rings.append(ring.coords) - else: - rings.append(list(ring.coords)[::-1]) - for ring in polygon.interiors: - if signed_area(ring.coords) / s <= 0: - rings.append(ring.coords) - else: - rings.append(list(ring.coords)[::-1]) - return Polygon(shell=rings[0], holes=rings[1:]) + shell = get_oriented_ring(polygon.exterior.coords) + s = -s # flip orientation for holes + holes = [get_oriented_ring(ring.coords) for ring in polygon.interiors] + return Polygon(shell=shell, holes=holes) def box( From 5b461c776cad5568088756555346a1ae2ae8b52e Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Tue, 31 Oct 2023 19:58:27 +0000 Subject: [PATCH 21/41] pass ccw through to get_oriented_ring --- pygeoif/factories.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pygeoif/factories.py b/pygeoif/factories.py index 3e0ffa38..e7d762f8 100644 --- a/pygeoif/factories.py +++ b/pygeoif/factories.py @@ -60,6 +60,11 @@ mpre: Pattern[str] = re.compile(r"\(\((.+?)\)\)") +def get_oriented_ring(ring: LineType, ccw: bool) -> LineType: # noqa: FBT001 + s = 1.0 if ccw else -1.0 + return ring if signed_area(ring) / s >= 0 else ring[::-1] + + def orient(polygon: Polygon, ccw: bool = True) -> Polygon: # noqa: FBT001, FBT002 """ Return a polygon with exteriors and interiors in the right orientation. @@ -68,14 +73,9 @@ def orient(polygon: Polygon, ccw: bool = True) -> Polygon: # noqa: FBT001, FBT0 and the interiors will be in clockwise orientation, or the other way round when ccw is False. """ - - def get_oriented_ring(ring: LineType) -> LineType: - return ring if signed_area(ring) / s >= 0 else ring[::-1] - - s = 1.0 if ccw else -1.0 - shell = get_oriented_ring(polygon.exterior.coords) - s = -s # flip orientation for holes - holes = [get_oriented_ring(ring.coords) for ring in polygon.interiors] + shell = get_oriented_ring(polygon.exterior.coords, ccw) + ccw = not ccw # flip orientation for holes + holes = [get_oriented_ring(ring.coords, ccw) for ring in polygon.interiors] return Polygon(shell=shell, holes=holes) From 0fdc579d5d652cd02915573739ef103ddf74b1e9 Mon Sep 17 00:00:00 2001 From: Alex Svetkin Date: Wed, 1 Nov 2023 00:38:44 +0100 Subject: [PATCH 22/41] added force_2d factory and move_coordinates function #180 --- pygeoif/factories.py | 32 +++++++++++ pygeoif/functions.py | 51 ++++++++++++++++++ tests/test_factories.py | 114 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 197 insertions(+) diff --git a/pygeoif/factories.py b/pygeoif/factories.py index e7d762f8..8377b309 100644 --- a/pygeoif/factories.py +++ b/pygeoif/factories.py @@ -24,6 +24,7 @@ from typing import cast from pygeoif.exceptions import WKTParserError +from pygeoif.functions import move_coordinates from pygeoif.functions import signed_area from pygeoif.geometry import Geometry from pygeoif.geometry import GeometryCollection @@ -60,6 +61,36 @@ mpre: Pattern[str] = re.compile(r"\(\((.+?)\)\)") +def force_2d( + context: Union[GeoType, GeoCollectionType], +) -> Union[Geometry, GeometryCollection]: + """ + Force the dimensionality of a geometry to 2D. + + >>> force_2d(Point(0, 0, 1)) + Point(0, 0) + >>> force_2d(Point(0, 0)) + Point(0, 0) + >>> force_2d(LineString([(0, 0, 0), (0, 1, 1), (1, 1, 2)])) + LineString(((0, 0), (0, 1), (1, 1))) + """ + geometry = context if isinstance(context, dict) else mapping(context) + if not geometry: + msg = "Object does not implement __geo_interface__" + raise TypeError(msg) + if geometry["type"] == "GeometryCollection": + return GeometryCollection( + force_2d(g) # type: ignore [arg-type] + for g in geometry["geometries"] # type: ignore [typeddict-item] + ) + + geometry["coordinates"] = move_coordinates( # type: ignore [typeddict-unknown-key] + geometry["coordinates"], # type: ignore [typeddict-item] + (0, 0), + ) + return shape(geometry) + + def get_oriented_ring(ring: LineType, ccw: bool) -> LineType: # noqa: FBT001 s = 1.0 if ccw else -1.0 return ring if signed_area(ring) / s >= 0 else ring[::-1] @@ -332,6 +363,7 @@ def mapping( __all__ = [ + "force_2d", "box", "from_wkt", "mapping", diff --git a/pygeoif/functions.py b/pygeoif/functions.py index f3e82f34..1d771285 100644 --- a/pygeoif/functions.py +++ b/pygeoif/functions.py @@ -19,8 +19,10 @@ import math from itertools import groupby from itertools import zip_longest +from typing import Any from typing import Iterable from typing import List +from typing import Sequence from typing import Tuple from typing import Union from typing import cast @@ -188,11 +190,60 @@ def compare_geo_interface( return False +def move_coordinate( + coordinate: Sequence[float], + move_by: Sequence[float], + z: float = 0, +) -> Tuple[float, ...]: + """ + Move the coordinate by the given vector. + + This forcefully changes the dimensions of the coordinate to match the latter. + >>> move_coordinate((0, 0), (-1, 1)) + (-1, 1) + >>> move_coordinate((0, 0, 0), (-1, 1)) + (-1, 1) + >>> move_coordinate((0, 0), (-1, 1, 0)) + (-1, 1, 0) + """ + if len(coordinate) > len(move_by): + return tuple(c + m for c, m in zip(coordinate, move_by)) + return tuple(c + m for c, m in zip_longest(coordinate, move_by, fillvalue=z)) + + +def move_coordinates( + coordinates: Sequence[Any], + move_by: Sequence[float], + z: float = 0, +) -> Sequence[Any]: + """ + Move the coordinates recursively by the given vector. + + This forcefully changes the dimension of each of the coordinate to match + the dimension of the vector. + >>> move_coordinates(((0, 0), (-1, 1)), (-1, 1)) + ((-1, 1), (-2, 2)) + >>> move_coordinates(((0, 0, 0), (-1, 1, 0)), (-1, 1)) + ((-1, 1), (-2, 2)) + >>> move_coordinates(((0, 0), (-1, 1)), (-1, 1, 0)) + ((-1, 1, 0), (-2, 2, 0)) + """ + if isinstance(coordinates, (tuple, list)) and isinstance( + coordinates[0], + (int, float), + ): + # coordinates is just a list of numbers, i.e. represents a single coordinate + return move_coordinate(coordinates, move_by, z) + return tuple(move_coordinates(c, move_by, z) for c in coordinates) + + __all__ = [ "centroid", "compare_coordinates", "compare_geo_interface", "convex_hull", "dedupe", + "move_coordinate", + "move_coordinates", "signed_area", ] diff --git a/tests/test_factories.py b/tests/test_factories.py index 96235841..c2472827 100644 --- a/tests/test_factories.py +++ b/tests/test_factories.py @@ -21,6 +21,120 @@ def test_num_float() -> None: assert isinstance(factories.num("1.1"), float) +def test_force_2d_point() -> None: + # 2d point to 2d point (no actual change) + p = geometry.Point(-1, 1) + p2d = factories.force_2d(p) + assert p2d.x == -1 + assert p2d.y == 1 + assert not p2d.has_z + + # 3d point to 2d point + p = geometry.Point(-1, 1, 2) + p2d = factories.force_2d(p) + assert p2d.x == -1 + assert p2d.y == 1 + assert not p2d.has_z + + +def test_force_2d_multipoint() -> None: + # 2d to 2d (no actual change) + p = geometry.MultiPoint([(-1, 1), (2, 3)]) + p2d = factories.force_2d(p) + assert list(p2d.geoms) == [geometry.Point(-1, 1), geometry.Point(2, 3)] + + +def test_force_2d_linestring() -> None: + # 2d line string to 2d line string (no actual change) + ls = geometry.LineString([(1, 2), (3, 4)]) + l2d = factories.force_2d(ls) + assert l2d.coords == ((1, 2), (3, 4)) + + # 3d line string to 2d line string + ls = geometry.LineString([(1, 2, 3), (4, 5, 6)]) + l2d = factories.force_2d(ls) + assert l2d.coords == ((1, 2), (4, 5)) + + +def test_force_2d_linearring() -> None: + # 2d linear ring to 2d linear ring (no actual change) + r = geometry.LinearRing([(1, 2), (3, 4)]) + r2d = factories.force_2d(r) + assert r2d.coords == ((1, 2), (3, 4), (1, 2)) + + # 3d linear ring to 2d linear ring + r = geometry.LinearRing([(1, 2, 3), (4, 5, 6)]) + r2d = factories.force_2d(r) + assert r2d.coords == ((1, 2), (4, 5), (1, 2)) + + +def test_force_2d_multilinestring() -> None: + # 2d multi line string to 2d multi line string (no actual change) + mls = geometry.MultiLineString([[(1, 2), (3, 4)], [(5, 6), (7, 8)]]) + mls2d = factories.force_2d(mls) + assert list(mls2d.geoms) == list(mls.geoms) + + # 3d multi line string to 2d multi line string + mls = geometry.MultiLineString([[(1, 2, 3), (4, 5, 6)], [(7, 8, 9), (10, 11, 12)]]) + mls2d = factories.force_2d(mls) + assert list(mls2d.geoms) == [geometry.LineString([(1, 2), (4, 5)]), geometry.LineString([(7, 8), (10, 11)])] + +def test_force_2d_polygon() -> None: + # 2d to 2d (no actual change) + external = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] + internal = [(0.5, 0.5), (0.5, 1.5), (1.5, 1.5), (1.5, 0.5), (0.5, 0.5)] + p = geometry.Polygon(external, [internal]) + p2d = factories.force_2d(p) + assert p2d.coords[0] == ( + ((0, 0), (0, 2), (2, 2), (2, 0), (0, 0)) + ) + assert p2d.coords[1] == ( + ((0.5, 0.5), (0.5, 1.5), (1.5, 1.5), (1.5, 0.5), (0.5, 0.5)), + ) + assert not p2d.has_z + assert p.maybe_valid == p2d.maybe_valid + + # 3d to 2d + external = [(0, 0, 1), (0, 2, 1), (2, 2, 1), (2, 0, 1), (0, 0, 1)] + internal = [(0.5, 0.5, 1), (0.5, 1.5, 1), (1.5, 1.5, 1), (1.5, 0.5, 1), (0.5, 0.5, 1)] + + p = geometry.Polygon(external, [internal]) + p2d = factories.force_2d(p) + assert p2d.coords[0] == ( + ((0, 0), (0, 2), (2, 2), (2, 0), (0, 0)) + ) + assert p2d.coords[1] == ( + ((0.5, 0.5), (0.5, 1.5), (1.5, 1.5), (1.5, 0.5), (0.5, 0.5)), + ) + assert not p2d.has_z + + +def test_force_2d_multipolygon() -> None: + # 2d to 2d (no actual change) + external = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] + internal = [(0.5, 0.5), (0.5, 1.5), (1.5, 1.5), (1.5, 0.5), (0.5, 0.5)] + mp = geometry.MultiPolygon([(external, [internal]), (external, [internal])]) + mp2d = factories.force_2d(mp) + + assert list(mp2d.geoms) == list(mp.geoms) + + +def test_force2d_collection() -> None: + # 2d to 2d (no actual change) + gc = geometry.GeometryCollection([geometry.Point(-1, 1), geometry.Point(-2, 2)]) + gc2d = factories.force_2d(gc) + assert list(gc2d.geoms) == list(gc.geoms) + + # 3d to 2d + gc = geometry.GeometryCollection([geometry.Point(-1, 1, 0), geometry.Point(-2, 2, 0)]) + gc2d = factories.force_2d(gc) + assert list(gc2d.geoms) == [geometry.Point(-1, 1), geometry.Point(-2, 2)] + + +def test_force_2d_nongeo() -> None: + pytest.raises(AttributeError, factories.force_2d, (1, 2, 3)) + + def test_orient_true() -> None: ext = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] int_1 = [(0.5, 0.25), (1.5, 0.25), (1.5, 1.25), (0.5, 1.25), (0.5, 0.25)] From f28889f8969e9f874b08a09042c08aff60dec833 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 31 Oct 2023 23:46:40 +0000 Subject: [PATCH 23/41] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pygeoif/factories.py | 2 +- tests/test_factories.py | 26 +++++++++++++++++--------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/pygeoif/factories.py b/pygeoif/factories.py index 8377b309..4f7da1b8 100644 --- a/pygeoif/factories.py +++ b/pygeoif/factories.py @@ -86,7 +86,7 @@ def force_2d( geometry["coordinates"] = move_coordinates( # type: ignore [typeddict-unknown-key] geometry["coordinates"], # type: ignore [typeddict-item] - (0, 0), + (0, 0), ) return shape(geometry) diff --git a/tests/test_factories.py b/tests/test_factories.py index c2472827..99b30b6c 100644 --- a/tests/test_factories.py +++ b/tests/test_factories.py @@ -77,7 +77,11 @@ def test_force_2d_multilinestring() -> None: # 3d multi line string to 2d multi line string mls = geometry.MultiLineString([[(1, 2, 3), (4, 5, 6)], [(7, 8, 9), (10, 11, 12)]]) mls2d = factories.force_2d(mls) - assert list(mls2d.geoms) == [geometry.LineString([(1, 2), (4, 5)]), geometry.LineString([(7, 8), (10, 11)])] + assert list(mls2d.geoms) == [ + geometry.LineString([(1, 2), (4, 5)]), + geometry.LineString([(7, 8), (10, 11)]), + ] + def test_force_2d_polygon() -> None: # 2d to 2d (no actual change) @@ -85,9 +89,7 @@ def test_force_2d_polygon() -> None: internal = [(0.5, 0.5), (0.5, 1.5), (1.5, 1.5), (1.5, 0.5), (0.5, 0.5)] p = geometry.Polygon(external, [internal]) p2d = factories.force_2d(p) - assert p2d.coords[0] == ( - ((0, 0), (0, 2), (2, 2), (2, 0), (0, 0)) - ) + assert p2d.coords[0] == (((0, 0), (0, 2), (2, 2), (2, 0), (0, 0))) assert p2d.coords[1] == ( ((0.5, 0.5), (0.5, 1.5), (1.5, 1.5), (1.5, 0.5), (0.5, 0.5)), ) @@ -96,13 +98,17 @@ def test_force_2d_polygon() -> None: # 3d to 2d external = [(0, 0, 1), (0, 2, 1), (2, 2, 1), (2, 0, 1), (0, 0, 1)] - internal = [(0.5, 0.5, 1), (0.5, 1.5, 1), (1.5, 1.5, 1), (1.5, 0.5, 1), (0.5, 0.5, 1)] + internal = [ + (0.5, 0.5, 1), + (0.5, 1.5, 1), + (1.5, 1.5, 1), + (1.5, 0.5, 1), + (0.5, 0.5, 1), + ] p = geometry.Polygon(external, [internal]) p2d = factories.force_2d(p) - assert p2d.coords[0] == ( - ((0, 0), (0, 2), (2, 2), (2, 0), (0, 0)) - ) + assert p2d.coords[0] == (((0, 0), (0, 2), (2, 2), (2, 0), (0, 0))) assert p2d.coords[1] == ( ((0.5, 0.5), (0.5, 1.5), (1.5, 1.5), (1.5, 0.5), (0.5, 0.5)), ) @@ -126,7 +132,9 @@ def test_force2d_collection() -> None: assert list(gc2d.geoms) == list(gc.geoms) # 3d to 2d - gc = geometry.GeometryCollection([geometry.Point(-1, 1, 0), geometry.Point(-2, 2, 0)]) + gc = geometry.GeometryCollection( + [geometry.Point(-1, 1, 0), geometry.Point(-2, 2, 0)], + ) gc2d = factories.force_2d(gc) assert list(gc2d.geoms) == [geometry.Point(-1, 1), geometry.Point(-2, 2)] From a654b38d5019d34b1f22208627987c359c4fbdc0 Mon Sep 17 00:00:00 2001 From: Alex Svetkin Date: Sat, 4 Nov 2023 18:41:55 +0100 Subject: [PATCH 24/41] introduced is_coordinate function --- pygeoif/functions.py | 25 +++++++++++++++++++------ tests/test_functions.py | 16 ++++++++++++++++ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/pygeoif/functions.py b/pygeoif/functions.py index 1d771285..034f669e 100644 --- a/pygeoif/functions.py +++ b/pygeoif/functions.py @@ -220,7 +220,7 @@ def move_coordinates( Move the coordinates recursively by the given vector. This forcefully changes the dimension of each of the coordinate to match - the dimension of the vector. + the dimensionality of the vector. >>> move_coordinates(((0, 0), (-1, 1)), (-1, 1)) ((-1, 1), (-2, 2)) >>> move_coordinates(((0, 0, 0), (-1, 1, 0)), (-1, 1)) @@ -228,15 +228,27 @@ def move_coordinates( >>> move_coordinates(((0, 0), (-1, 1)), (-1, 1, 0)) ((-1, 1, 0), (-2, 2, 0)) """ - if isinstance(coordinates, (tuple, list)) and isinstance( - coordinates[0], - (int, float), - ): - # coordinates is just a list of numbers, i.e. represents a single coordinate + if is_coordinate(coordinates): + # a single coordinate return move_coordinate(coordinates, move_by, z) + # a list of coordinates return tuple(move_coordinates(c, move_by, z) for c in coordinates) +def is_coordinate(val: Any) -> bool: # noqa: ANN401 + """ + Check if given value is a coordinate i.e. vector of generic dimensionality. + + >>> is_coordinate((1, 0)) + True + >>> is_coordinate(1) + False + >>> is_coordinate([(1, 2), (3, 4)]) + False + """ + return isinstance(val, tuple) and all(isinstance(x, (int, float)) for x in val) + + __all__ = [ "centroid", "compare_coordinates", @@ -245,5 +257,6 @@ def move_coordinates( "dedupe", "move_coordinate", "move_coordinates", + "is_coordinate", "signed_area", ] diff --git a/tests/test_functions.py b/tests/test_functions.py index e1441a22..8966e215 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -12,6 +12,7 @@ from pygeoif.functions import convex_hull from pygeoif.functions import dedupe from pygeoif.functions import signed_area +from pygeoif.functions import is_coordinate def circle_ish(x, y, r, steps): @@ -452,3 +453,18 @@ def test_compare_neq_empty_geo_interface() -> None: } assert compare_geo_interface(geo_if, {}) is False + + +def test_is_coordinate() -> None: + assert is_coordinate((1, 2)) is True + assert is_coordinate((1,)) is True + + +def test_is_coordinate_not_composite_coordinates() -> None: + assert is_coordinate([(1, 2)]) is False + assert is_coordinate(((1, 2), )) is False + assert is_coordinate((((1, 2), ), )) is False + + +def test_is_coordinate_not_primitive() -> None: + assert is_coordinate(1) is False From 8466fd998b5053079d9c641864b3bbadf7248ef3 Mon Sep 17 00:00:00 2001 From: Alex Svetkin Date: Sun, 5 Nov 2023 10:31:32 +0100 Subject: [PATCH 25/41] added force_3d factory --- pygeoif/factories.py | 35 +++++++++++++++++++++++++++++++++++ tests/test_factories.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/pygeoif/factories.py b/pygeoif/factories.py index 4f7da1b8..fc584213 100644 --- a/pygeoif/factories.py +++ b/pygeoif/factories.py @@ -91,6 +91,40 @@ def force_2d( return shape(geometry) +def force_3d( + context: Union[GeoType, GeoCollectionType], + z: float = 0, +) -> Union[Geometry, GeometryCollection]: + """ + Force the dimensionality of a geometry to 3D. + + >>> force_3d(Point(0, 0)) + Point(0, 0, 0) + >>> force_3d(Point(0, 0), 1) + Point(0, 0, 1) + >>> force_3d(Point(0, 0, 0)) + Point(0, 0, 0) + >>> force_3d(LineString([(0, 0), (0, 1), (1, 1)])) + LineString(((0, 0, 0), (0, 1, 0), (1, 1, 0))) + """ + geometry = context if isinstance(context, dict) else mapping(context) + if not geometry: + msg = "Object does not implement __geo_interface__" + raise TypeError(msg) + if geometry["type"] == "GeometryCollection": + return GeometryCollection( + force_3d(g, z) # type: ignore [arg-type] + for g in geometry["geometries"] # type: ignore [typeddict-item] + ) + + geometry["coordinates"] = move_coordinates( # type: ignore [typeddict-unknown-key] + geometry["coordinates"], # type: ignore [typeddict-item] + (0, 0, 0), + z, + ) + return shape(geometry) + + def get_oriented_ring(ring: LineType, ccw: bool) -> LineType: # noqa: FBT001 s = 1.0 if ccw else -1.0 return ring if signed_area(ring) / s >= 0 else ring[::-1] @@ -364,6 +398,7 @@ def mapping( __all__ = [ "force_2d", + "force_3d", "box", "from_wkt", "mapping", diff --git a/tests/test_factories.py b/tests/test_factories.py index 99b30b6c..71994ac1 100644 --- a/tests/test_factories.py +++ b/tests/test_factories.py @@ -143,6 +143,45 @@ def test_force_2d_nongeo() -> None: pytest.raises(AttributeError, factories.force_2d, (1, 2, 3)) +def test_force_3d_point() -> None: + p = geometry.Point(0, 0) + p3d = factories.force_3d(p) + assert p3d.x == 0 + assert p3d.y == 0 + assert p3d.z == 0 + assert p3d.has_z + + +def test_force_3d_collection() -> None: + gc = geometry.GeometryCollection( + [geometry.Point(-1, 1), geometry.Point(-2, 2)], + ) + gc3d = factories.force_3d(gc) + assert list(gc3d.geoms) == [geometry.Point(-1, 1, 0), geometry.Point(-2, 2, 0)] + + +def test_force_3d_point_with_z() -> None: + p = geometry.Point(0, 0, 1) + p3d = factories.force_3d(p) + assert p3d.x == 0 + assert p3d.y == 0 + assert p3d.z == 1 + assert p3d.has_z + + +def test_force_3d_point_noop() -> None: + p = geometry.Point(1, 2, 3) + p3d = factories.force_3d(p) + assert p3d.x == 1 + assert p3d.y == 2 + assert p3d.z == 3 + assert p3d.has_z + + +def test_force_3d_nongeo() -> None: + pytest.raises(AttributeError, factories.force_3d, (1, 2)) + + def test_orient_true() -> None: ext = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] int_1 = [(0.5, 0.25), (1.5, 0.25), (1.5, 1.25), (0.5, 1.25), (0.5, 0.25)] From a21390578637cc43b5bdf4a7cffb9b713eb81180 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 5 Nov 2023 09:49:23 +0000 Subject: [PATCH 26/41] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_functions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_functions.py b/tests/test_functions.py index 8966e215..3267ea66 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -11,8 +11,8 @@ from pygeoif.functions import compare_geo_interface from pygeoif.functions import convex_hull from pygeoif.functions import dedupe -from pygeoif.functions import signed_area from pygeoif.functions import is_coordinate +from pygeoif.functions import signed_area def circle_ish(x, y, r, steps): @@ -462,8 +462,8 @@ def test_is_coordinate() -> None: def test_is_coordinate_not_composite_coordinates() -> None: assert is_coordinate([(1, 2)]) is False - assert is_coordinate(((1, 2), )) is False - assert is_coordinate((((1, 2), ), )) is False + assert is_coordinate(((1, 2),)) is False + assert is_coordinate((((1, 2),),)) is False def test_is_coordinate_not_primitive() -> None: From 31e77334c54d3f4685c112823a25d6dd598874f1 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sun, 5 Nov 2023 18:41:20 +0000 Subject: [PATCH 27/41] simplify code --- pygeoif/factories.py | 103 +++++++++++++++------------------------- pygeoif/feature.py | 8 ++-- pygeoif/functions.py | 71 +++++++++++++++------------ pygeoif/types.py | 45 +++++++----------- pyproject.toml | 3 +- tests/test_functions.py | 16 ------- 6 files changed, 100 insertions(+), 146 deletions(-) diff --git a/pygeoif/factories.py b/pygeoif/factories.py index fc584213..b0f61450 100644 --- a/pygeoif/factories.py +++ b/pygeoif/factories.py @@ -24,7 +24,7 @@ from typing import cast from pygeoif.exceptions import WKTParserError -from pygeoif.functions import move_coordinates +from pygeoif.functions import move_geo_interface from pygeoif.functions import signed_area from pygeoif.geometry import Geometry from pygeoif.geometry import GeometryCollection @@ -61,70 +61,6 @@ mpre: Pattern[str] = re.compile(r"\(\((.+?)\)\)") -def force_2d( - context: Union[GeoType, GeoCollectionType], -) -> Union[Geometry, GeometryCollection]: - """ - Force the dimensionality of a geometry to 2D. - - >>> force_2d(Point(0, 0, 1)) - Point(0, 0) - >>> force_2d(Point(0, 0)) - Point(0, 0) - >>> force_2d(LineString([(0, 0, 0), (0, 1, 1), (1, 1, 2)])) - LineString(((0, 0), (0, 1), (1, 1))) - """ - geometry = context if isinstance(context, dict) else mapping(context) - if not geometry: - msg = "Object does not implement __geo_interface__" - raise TypeError(msg) - if geometry["type"] == "GeometryCollection": - return GeometryCollection( - force_2d(g) # type: ignore [arg-type] - for g in geometry["geometries"] # type: ignore [typeddict-item] - ) - - geometry["coordinates"] = move_coordinates( # type: ignore [typeddict-unknown-key] - geometry["coordinates"], # type: ignore [typeddict-item] - (0, 0), - ) - return shape(geometry) - - -def force_3d( - context: Union[GeoType, GeoCollectionType], - z: float = 0, -) -> Union[Geometry, GeometryCollection]: - """ - Force the dimensionality of a geometry to 3D. - - >>> force_3d(Point(0, 0)) - Point(0, 0, 0) - >>> force_3d(Point(0, 0), 1) - Point(0, 0, 1) - >>> force_3d(Point(0, 0, 0)) - Point(0, 0, 0) - >>> force_3d(LineString([(0, 0), (0, 1), (1, 1)])) - LineString(((0, 0, 0), (0, 1, 0), (1, 1, 0))) - """ - geometry = context if isinstance(context, dict) else mapping(context) - if not geometry: - msg = "Object does not implement __geo_interface__" - raise TypeError(msg) - if geometry["type"] == "GeometryCollection": - return GeometryCollection( - force_3d(g, z) # type: ignore [arg-type] - for g in geometry["geometries"] # type: ignore [typeddict-item] - ) - - geometry["coordinates"] = move_coordinates( # type: ignore [typeddict-unknown-key] - geometry["coordinates"], # type: ignore [typeddict-item] - (0, 0, 0), - z, - ) - return shape(geometry) - - def get_oriented_ring(ring: LineType, ccw: bool) -> LineType: # noqa: FBT001 s = 1.0 if ccw else -1.0 return ring if signed_area(ring) / s >= 0 else ring[::-1] @@ -396,6 +332,43 @@ def mapping( return ob.__geo_interface__ +def force_2d( + context: Union[GeoType, GeoCollectionType], +) -> Union[Geometry, GeometryCollection]: + """ + Force the dimensionality of a geometry to 2D. + + >>> force_2d(Point(0, 0, 1)) + Point(0, 0) + >>> force_2d(Point(0, 0)) + Point(0, 0) + >>> force_2d(LineString([(0, 0, 0), (0, 1, 1), (1, 1, 2)])) + LineString(((0, 0), (0, 1), (1, 1))) + """ + geometry = mapping(context) + return shape(move_geo_interface(geometry, (0, 0))) + + +def force_3d( + context: Union[GeoType, GeoCollectionType], + z: float = 0, +) -> Union[Geometry, GeometryCollection]: + """ + Force the dimensionality of a geometry to 3D. + + >>> force_3d(Point(0, 0)) + Point(0, 0, 0) + >>> force_3d(Point(0, 0), 1) + Point(0, 0, 1) + >>> force_3d(Point(0, 0, 0)) + Point(0, 0, 0) + >>> force_3d(LineString([(0, 0), (0, 1), (1, 1)])) + LineString(((0, 0, 0), (0, 1, 0), (1, 1, 0))) + """ + geometry = mapping(context) + return shape(move_geo_interface(geometry, (0, 0, z))) + + __all__ = [ "force_2d", "force_3d", diff --git a/pygeoif/feature.py b/pygeoif/feature.py index 23a45173..633d51d6 100644 --- a/pygeoif/feature.py +++ b/pygeoif/feature.py @@ -46,9 +46,9 @@ def feature_geo_interface_equals( my_interface["geometry"]["type"] == other_interface["geometry"].get("type"), compare_coordinates( coords=my_interface["geometry"]["coordinates"], - other=other_interface["geometry"].get( + other=other_interface["geometry"].get( # type: ignore [arg-type] "coordinates", - ), # type: ignore[arg-type] + ), ), ], ) @@ -128,7 +128,7 @@ def properties(self) -> Dict[str, Any]: def __geo_interface__(self) -> GeoFeatureInterface: """Return the GeoInterface of the geometry with properties.""" geo_interface: GeoFeatureInterface = { - "type": self.__class__.__name__, + "type": "Feature", "bbox": cast(Bounds, self._geometry.bounds), "geometry": self._geometry.__geo_interface__, "properties": self._properties, @@ -220,7 +220,7 @@ def bounds(self) -> Bounds: def __geo_interface__(self) -> GeoFeatureCollectionInterface: """Return the GeoInterface of the feature.""" return { - "type": self.__class__.__name__, + "type": "FeatureCollection", "bbox": self.bounds, "features": tuple(feature.__geo_interface__ for feature in self._features), } diff --git a/pygeoif/functions.py b/pygeoif/functions.py index 034f669e..d9f5190d 100644 --- a/pygeoif/functions.py +++ b/pygeoif/functions.py @@ -19,10 +19,8 @@ import math from itertools import groupby from itertools import zip_longest -from typing import Any from typing import Iterable from typing import List -from typing import Sequence from typing import Tuple from typing import Union from typing import cast @@ -33,6 +31,7 @@ from pygeoif.types import LineType from pygeoif.types import MultiCoordinatesType from pygeoif.types import Point2D +from pygeoif.types import PointType def signed_area(coords: LineType) -> float: @@ -191,10 +190,9 @@ def compare_geo_interface( def move_coordinate( - coordinate: Sequence[float], - move_by: Sequence[float], - z: float = 0, -) -> Tuple[float, ...]: + coordinate: PointType, + move_by: PointType, +) -> PointType: """ Move the coordinate by the given vector. @@ -206,16 +204,19 @@ def move_coordinate( >>> move_coordinate((0, 0), (-1, 1, 0)) (-1, 1, 0) """ - if len(coordinate) > len(move_by): - return tuple(c + m for c, m in zip(coordinate, move_by)) - return tuple(c + m for c, m in zip_longest(coordinate, move_by, fillvalue=z)) + if len(coordinate) < len(move_by): + return cast( + PointType, + tuple(c + m for c, m in zip_longest(coordinate, move_by, fillvalue=0)), + ) + + return cast(PointType, tuple(c + m for c, m in zip(coordinate, move_by))) def move_coordinates( - coordinates: Sequence[Any], - move_by: Sequence[float], - z: float = 0, -) -> Sequence[Any]: + coordinates: CoordinatesType, + move_by: PointType, +) -> CoordinatesType: """ Move the coordinates recursively by the given vector. @@ -228,25 +229,34 @@ def move_coordinates( >>> move_coordinates(((0, 0), (-1, 1)), (-1, 1, 0)) ((-1, 1, 0), (-2, 2, 0)) """ - if is_coordinate(coordinates): - # a single coordinate - return move_coordinate(coordinates, move_by, z) - # a list of coordinates - return tuple(move_coordinates(c, move_by, z) for c in coordinates) + if isinstance(coordinates[0], (int, float)): + return move_coordinate(cast(PointType, coordinates), move_by) + return cast( + CoordinatesType, + tuple(move_coordinates(cast(CoordinatesType, c), move_by) for c in coordinates), + ) -def is_coordinate(val: Any) -> bool: # noqa: ANN401 - """ - Check if given value is a coordinate i.e. vector of generic dimensionality. - - >>> is_coordinate((1, 0)) - True - >>> is_coordinate(1) - False - >>> is_coordinate([(1, 2), (3, 4)]) - False - """ - return isinstance(val, tuple) and all(isinstance(x, (int, float)) for x in val) +def move_geo_interface( + interface: Union[GeoInterface, GeoCollectionInterface], + move_by: PointType, +) -> Union[GeoInterface, GeoCollectionInterface]: + """Move the coordinates of the geo interface by the given vector.""" + if interface["type"] == "GeometryCollection": + return { + "type": "GeometryCollection", + "geometries": tuple( + move_geo_interface(g, move_by) + for g in interface["geometries"] # type: ignore [typeddict-item] + ), + } + return { + "type": interface["type"], + "coordinates": move_coordinates( + interface["coordinates"], # type: ignore [typeddict-item, arg-type] + move_by, + ), + } __all__ = [ @@ -257,6 +267,5 @@ def is_coordinate(val: Any) -> bool: # noqa: ANN401 "dedupe", "move_coordinate", "move_coordinates", - "is_coordinate", "signed_area", ] diff --git a/pygeoif/types.py b/pygeoif/types.py index 861781e8..e9de16f6 100644 --- a/pygeoif/types.py +++ b/pygeoif/types.py @@ -23,8 +23,9 @@ from typing import Union from typing_extensions import Literal +from typing_extensions import NotRequired from typing_extensions import Protocol -from typing_extensions import TypedDict +from typing_extensions import TypedDict # for Python <3.11 with (Not)Required Point2D = Tuple[float, float] Point3D = Tuple[float, float, float] @@ -48,17 +49,12 @@ MultiCoordinatesType = Sequence[CoordinatesType] -class GeoInterfaceBase(TypedDict): +class GeoInterface(TypedDict): """Required keys for the GeoInterface.""" type: str coordinates: Union[CoordinatesType, MultiCoordinatesType] - - -class GeoInterface(GeoInterfaceBase, total=False): - """GeoInterface provides an optional bbox.""" - - bbox: Bounds + bbox: NotRequired[Bounds] class GeoCollectionInterface(TypedDict): @@ -66,35 +62,26 @@ class GeoCollectionInterface(TypedDict): type: Literal["GeometryCollection"] geometries: Sequence[Union[GeoInterface, "GeoCollectionInterface"]] + bbox: NotRequired[Bounds] -class GeoFeatureInterfaceBase(TypedDict): - """Required keys for the GeoInterface for Features.""" - - type: str - geometry: GeoInterface - - -class GeoFeatureInterface(GeoFeatureInterfaceBase, total=False): +class GeoFeatureInterface(TypedDict): """The GeoFeatureInterface has optional keys.""" - bbox: Bounds - properties: Dict[str, Any] - id: Union[str, int] # noqa: A003 - - -class GeoFeatureCollectionInterfaceBase(TypedDict): - """Required Keys for the GeoInterface of a FeatureCollection.""" - - type: str - features: Sequence[GeoFeatureInterface] + type: Literal["Feature"] + bbox: NotRequired[Bounds] + properties: NotRequired[Dict[str, Any]] + id: NotRequired[Union[str, int]] + geometry: GeoInterface -class GeoFeatureCollectionInterface(GeoFeatureCollectionInterfaceBase, total=False): +class GeoFeatureCollectionInterface(TypedDict): """Bbox and id are optional keys for the GeoFeatureCollectionInterface.""" - bbox: Bounds - id: Union[str, int] # noqa: A003 + type: Literal["FeatureCollection"] + features: Sequence[GeoFeatureInterface] + bbox: NotRequired[Bounds] + id: NotRequired[Union[str, int]] class GeoType(Protocol): diff --git a/pyproject.toml b/pyproject.toml index 0101bf95..1facfba7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ dev = [ "pygeoif[complexity]", "pygeoif[linting]", "pygeoif[tests]", + "pygeoif[typing]", ] linting = [ "black", @@ -210,7 +211,7 @@ select = [ "W", "YTT", ] -target-version = "py37" +target-version = "py38" [tool.ruff.isort] force-single-line = true diff --git a/tests/test_functions.py b/tests/test_functions.py index 3267ea66..e1441a22 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -11,7 +11,6 @@ from pygeoif.functions import compare_geo_interface from pygeoif.functions import convex_hull from pygeoif.functions import dedupe -from pygeoif.functions import is_coordinate from pygeoif.functions import signed_area @@ -453,18 +452,3 @@ def test_compare_neq_empty_geo_interface() -> None: } assert compare_geo_interface(geo_if, {}) is False - - -def test_is_coordinate() -> None: - assert is_coordinate((1, 2)) is True - assert is_coordinate((1,)) is True - - -def test_is_coordinate_not_composite_coordinates() -> None: - assert is_coordinate([(1, 2)]) is False - assert is_coordinate(((1, 2),)) is False - assert is_coordinate((((1, 2),),)) is False - - -def test_is_coordinate_not_primitive() -> None: - assert is_coordinate(1) is False From f04b78d01b46c328e1b00091861dd15aeb75b48a Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sun, 5 Nov 2023 18:57:49 +0000 Subject: [PATCH 28/41] Update history add @whisk closes #180 --- docs/HISTORY.rst | 27 ++++++++++++++------------- pyproject.toml | 1 - 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/HISTORY.rst b/docs/HISTORY.rst index b347f272..3cec4bf8 100644 --- a/docs/HISTORY.rst +++ b/docs/HISTORY.rst @@ -6,11 +6,12 @@ Changelog - remove Python 3.7 support - Geometries are now immutable and hashable + - add ``force_2d`` and ``force_3d`` factories [Alex Svetkin] 1.1.1 (2023/10/27) ------------------ - - Use pyproject.toml instead of setup.py + - Use ``pyproject.toml``, remove ``setup.py`` and ``MANIFEST.in`` 1.1 (2023/10/13) ----------------- @@ -28,8 +29,8 @@ Changelog - changes to keep functionality and interface close to ``shapely`` - remove support for python 2 - minimum python version is 3.7 -- rename as_shape to shape -- add box factory +- rename ``as_shape`` to ``shape`` +- add ``box`` factory - format with black - reconstruct objects from their representation - Parse WKT that is not in upper case. @@ -43,7 +44,7 @@ Changelog ----------------- - fix broken multipolygon [mindflayer] -- add "bbox" to `__geo_interface__` output [jzmiller1] +- add "bbox" to ``__geo_interface__`` output [jzmiller1] 0.6 (2015/08/04) ----------------- @@ -53,7 +54,7 @@ Changelog 0.5 (2015/07/13) ----------------- -- Add __iter__ method to FeatureCollection and GeometryCollection [jzmiller1]. +- Add ``__iter__`` method to FeatureCollection and GeometryCollection [jzmiller1]. - add pypy and pypy3 and python 3.4 to travis. - Add tox configuration for performing local testing [Ian Lee]. - Add Travis continuous deployment. @@ -61,8 +62,8 @@ Changelog 0.4 (2013/10/25) ----------------- -- after a year in production promote it to `Development Status :: 5 - Production/Stable` -- MultiPolygons return tuples as the __geo_interface__ +- after a year in production promote it to ``Development Status :: 5 - Production/Stable`` +- MultiPolygons return tuples as the ``__geo_interface__`` 0.3.1 (2012/11/15) ------------------ @@ -76,16 +77,16 @@ Changelog - add GeometryCollection - len(Multi*) and len(GeometryCollection) returns the number of contained Geometries -- add orient function to get clockwise or counterclockwise oriented polygons -- add signed_area function -- add _set_orientation method to lineStrings, Polygons and MultiPolygons +- add ``orient`` function to get clockwise or counterclockwise oriented polygons +- add ``signed_area`` function +- add ``_set_orientation`` method to lineStrings, Polygons and MultiPolygons 0.2.1 (2012/08/02) ------------------- -- as_shape also accepts an object that is neither a dictionary nor has a __geo_interface__ - but can be converted into a __geo_interface__ compliant dictionary +- ``as_shape`` also accepts an object that is neither a dictionary nor has a ``__geo_interface__`` + but can be converted into a ``__geo_interface__`` compliant dictionary 0.2 (2012/08/01) @@ -93,7 +94,7 @@ Changelog - change license to LGPL - add wkt as a property -- as_shape also accepts a __geo_interface__ compliant dictionary +- ``as_shape`` also accepts a ``__geo_interface__`` compliant dictionary - test with python3 diff --git a/pyproject.toml b/pyproject.toml index 1facfba7..70e34a37 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,7 +69,6 @@ linting = [ "flake8-expression-complexity", "flake8-function-order", "flake8-length", - "flake8-literal", "flake8-pep3101", "flake8-rst-docstrings", "flake8-string-format", From c24953e0dc3ca4c5524218f779c471f555eb39cd Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sun, 5 Nov 2023 19:34:15 +0000 Subject: [PATCH 29/41] Add Changelog to urls --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 70e34a37..78a35d36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,6 +91,7 @@ content-type = "text/x-rst" file = "README.rst" [project.urls] +Changelog = "https://github.com/cleder/pygeoif/blob/develop/docs/HISTORY.rst" Documentation = "https://pygeoif.readthedocs.io/" Homepage = "https://github.com/cleder/pygeoif/" From 105912ca4a03d9a685365ff0005435cc1c9564ec Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Nov 2023 16:34:55 +0000 Subject: [PATCH 30/41] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.3 → v0.1.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.3...v0.1.4) - [github.com/python-jsonschema/check-jsonschema: 0.27.0 → 0.27.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.27.0...0.27.1) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 22ff14e3..f6c65955 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,7 +45,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.1.3' + rev: 'v0.1.4' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] @@ -89,7 +89,7 @@ repos: hooks: - id: pyprojectsort - repo: https://github.com/python-jsonschema/check-jsonschema - rev: "0.27.0" + rev: "0.27.1" hooks: - id: check-github-workflows - id: check-github-actions From ff2e39c27c9b36a604f54a28ccd9a7f3a861ce52 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Mon, 6 Nov 2023 18:28:29 +0000 Subject: [PATCH 31/41] add flake8 ignores --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index b627f37d..eacef83a 100644 --- a/tox.ini +++ b/tox.ini @@ -3,11 +3,11 @@ min_python_version = 3.7 exclude = .git,__pycache__,docs/source/conf.py,old,build,dist max_line_length = 89 ignore= - W503,I900,IF100,NQA102, RST212 + W503,I900,IF100,NQA102,RST212,LIT002 per-file-ignores = tests/*:S101,D103,S307,DALL000,S311,ECE001,FKA100 tests/test_geometrycollection.py: ECE001,S101,D103,S307,DALL000 - tests/test_factories.py: ECE001,S10,D10,S307,DALL000,PT009,T003 + tests/test_factories.py: ECE001,S10,D10,S307,DALL000,PT009,T003,CM001 tests/test_feature.py: ECE001,S10,D10,S307,DALL000,PT009,T003,P103 pygeoif/*: S604 pygeoif/types.py: A003 From 70f3b08c287d0047978729213b80db9779f266f1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 16:36:04 +0000 Subject: [PATCH 32/41] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.10.1 → 23.11.0](https://github.com/psf/black/compare/23.10.1...23.11.0) - [github.com/astral-sh/ruff-pre-commit: v0.1.4 → v0.1.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.4...v0.1.5) - [github.com/pre-commit/mirrors-mypy: v1.6.1 → v1.7.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.6.1...v1.7.0) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f6c65955..e3d96856 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,11 +41,11 @@ repos: hooks: - id: isort - repo: https://github.com/psf/black - rev: 23.10.1 + rev: 23.11.0 hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.1.4' + rev: 'v0.1.5' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] @@ -73,7 +73,7 @@ repos: - flake8-typing-imports - flake8-use-fstring - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.6.1 + rev: v1.7.0 hooks: - id: mypy # - repo: https://github.com/Lucas-C/pre-commit-hooks-markup From 45fdd8c7a2abefff7a307a35b49ecbe9befb553f Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Mon, 13 Nov 2023 17:45:09 +0000 Subject: [PATCH 33/41] update type annotations for mypy 1.7 --- pygeoif/functions.py | 9 +++------ pygeoif/geometry.py | 4 +--- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/pygeoif/functions.py b/pygeoif/functions.py index d9f5190d..19cd7b75 100644 --- a/pygeoif/functions.py +++ b/pygeoif/functions.py @@ -44,12 +44,9 @@ def signed_area(coords: LineType) -> float: xs, ys = map(list, zip(*(coord[:2] for coord in coords))) xs.append(xs[1]) # pragma: no mutate ys.append(ys[1]) # pragma: no mutate - return ( - sum( - xs[i] * (ys[i + 1] - ys[i - 1]) # type: ignore [operator] - for i in range(1, len(coords)) - ) - / 2.0 + return cast( + float, + sum(xs[i] * (ys[i + 1] - ys[i - 1]) for i in range(1, len(coords))) / 2.0, ) diff --git a/pygeoif/geometry.py b/pygeoif/geometry.py index 58ed051a..fdbe7164 100644 --- a/pygeoif/geometry.py +++ b/pygeoif/geometry.py @@ -955,9 +955,7 @@ def __init__(self, polygons: Sequence[PolygonType], unique: bool = False) -> Non tuple( Polygon( shell=polygon[0], - holes=polygon[1] # type: ignore [misc] - if len(polygon) == 2 # noqa: PLR2004 - else None, + holes=polygon[1] if len(polygon) == 2 else None, # noqa: PLR2004 ) for polygon in polygons ), From c75331b2c660971348fd9db2a952d95eda058eca Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 20 Nov 2023 16:34:39 +0000 Subject: [PATCH 34/41] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/hakancelikdev/unimport: 1.0.0 → 1.1.0](https://github.com/hakancelikdev/unimport/compare/1.0.0...1.1.0) - [github.com/astral-sh/ruff-pre-commit: v0.1.5 → v0.1.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.5...v0.1.6) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e3d96856..e822e498 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,7 +32,7 @@ repos: hooks: - id: absolufy-imports - repo: https://github.com/hakancelikdev/unimport - rev: 1.0.0 + rev: 1.1.0 hooks: - id: unimport args: [--remove, --include-star-import, --ignore-init, --gitignore] @@ -45,7 +45,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.1.5' + rev: 'v0.1.6' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 965f115bb139270dfbf40a0d7865d21ca3e66bd3 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sun, 26 Nov 2023 13:38:08 +0000 Subject: [PATCH 35/41] Remove hash method. it cannot be guaranteed that objects which compare equal have the same hash value --- .pre-commit-config.yaml | 5 ----- docs/HISTORY.rst | 2 +- pygeoif/geometry.py | 7 ------ tests/test_geometrycollection.py | 36 ------------------------------ tests/test_line.py | 6 ----- tests/test_linear_ring.py | 6 ----- tests/test_multiline.py | 8 ------- tests/test_multipoint.py | 6 ----- tests/test_multipolygon.py | 38 -------------------------------- tests/test_point.py | 12 ---------- tests/test_polygon.py | 6 ----- 11 files changed, 1 insertion(+), 131 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e822e498..b5a3bbcd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,11 +31,6 @@ repos: rev: v0.3.1 hooks: - id: absolufy-imports - - repo: https://github.com/hakancelikdev/unimport - rev: 1.1.0 - hooks: - - id: unimport - args: [--remove, --include-star-import, --ignore-init, --gitignore] - repo: https://github.com/pycqa/isort rev: 5.12.0 hooks: diff --git a/docs/HISTORY.rst b/docs/HISTORY.rst index 3cec4bf8..2ccdf5fd 100644 --- a/docs/HISTORY.rst +++ b/docs/HISTORY.rst @@ -5,7 +5,7 @@ Changelog ------------------ - remove Python 3.7 support - - Geometries are now immutable and hashable + - Geometries are now immutable (but not hashable) - add ``force_2d`` and ``force_3d`` factories [Alex Svetkin] 1.1.1 (2023/10/27) diff --git a/pygeoif/geometry.py b/pygeoif/geometry.py index fdbe7164..9b862777 100644 --- a/pygeoif/geometry.py +++ b/pygeoif/geometry.py @@ -67,9 +67,6 @@ def __delattr__(self, *args: Any) -> NoReturn: # noqa: ANN401 msg, ) - def __hash__(self) -> int: - return hash(self._geoms) - def __str__(self) -> str: return self.wkt @@ -1095,10 +1092,6 @@ def __eq__(self, other: object) -> bool: second=other.__geo_interface__, # type: ignore [attr-defined] ) - def __hash__(self) -> int: - """Return the hash of the collection.""" - return hash(self.wkt) - def __len__(self) -> int: """ Length of the collection. diff --git a/tests/test_geometrycollection.py b/tests/test_geometrycollection.py index 59065667..51b016cc 100644 --- a/tests/test_geometrycollection.py +++ b/tests/test_geometrycollection.py @@ -425,39 +425,3 @@ def test_nested_geometry_collection_repr_eval() -> None: ).__geo_interface__ == gc.__geo_interface__ ) - - -def test_nested_geometry_collection_hash() -> None: - multipoint = geometry.MultiPoint([(0, 0), (1, 1), (1, 2), (2, 2)]) - gc1 = geometry.GeometryCollection([geometry.Point(0, 0), multipoint]) - line1 = geometry.LineString([(0, 0), (3, 1)]) - gc2 = geometry.GeometryCollection([gc1, line1]) - poly1 = geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]) - e = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] - i = [(1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)] - poly2 = geometry.Polygon(e, [i]) - p0 = geometry.Point(0, 0) - p1 = geometry.Point(-1, -1) - ring = geometry.LinearRing([(0, 0), (1, 1), (1, 0), (0, 0)]) - line = geometry.LineString([(0, 0), (1, 1)]) - gc = geometry.GeometryCollection([gc2, poly1, poly2, p0, p1, ring, line]) - - assert hash(gc) == hash( - geometry.GeometryCollection( - [ - gc2, - poly1, - poly2, - p0, - p1, - ring, - line, - ], - ), - ) - - -def test_hash_empty() -> None: - gc = geometry.GeometryCollection([]) - - assert hash(gc) == hash(geometry.GeometryCollection([])) diff --git a/tests/test_line.py b/tests/test_line.py index 2d6fd5bf..b0b4df8a 100644 --- a/tests/test_line.py +++ b/tests/test_line.py @@ -19,12 +19,6 @@ def test_coords_get_3d() -> None: assert line.coords == ((0.0, 0.0, 0), (1.0, 1.0, 1)) -def test_hash() -> None: - line = geometry.LineString([(0, 0, 0), (1, 1, 1)]) - - assert hash(line) == hash(geometry.LineString([(0, 0, 0), (1, 1, 1)])) - - def test_set_geoms_raises() -> None: line = geometry.LineString([(0, 0), (1, 0)]) # pragma: no mutate diff --git a/tests/test_linear_ring.py b/tests/test_linear_ring.py index 3a67a879..8e4d3c9b 100644 --- a/tests/test_linear_ring.py +++ b/tests/test_linear_ring.py @@ -240,9 +240,3 @@ def test_empty_bounds() -> None: ring = geometry.LinearRing([]) assert ring.bounds == () - - -def test_hash() -> None: - ring = geometry.LinearRing([(0, 0), (4, 0), (4, 2), (0, 2)]) - - assert hash(ring) == hash(((0, 0), (4, 0), (4, 2), (0, 2), (0, 0))) diff --git a/tests/test_multiline.py b/tests/test_multiline.py index 0aadecae..40d14280 100644 --- a/tests/test_multiline.py +++ b/tests/test_multiline.py @@ -200,11 +200,3 @@ def test_empty_bounds() -> None: lines = geometry.MultiLineString([]) assert lines.bounds == () - - -def test_hash() -> None: - lines = geometry.MultiLineString(([(0, 0), (1, 1)], [[0.0, 0.0], [1.0, 2.0]])) - - assert hash(lines) == hash( - geometry.MultiLineString(([(0, 0), (1, 1)], [[0.0, 0.0], [1.0, 2.0]])), - ) diff --git a/tests/test_multipoint.py b/tests/test_multipoint.py index 9b64bef3..693bad36 100644 --- a/tests/test_multipoint.py +++ b/tests/test_multipoint.py @@ -148,12 +148,6 @@ def test_from_points_unique() -> None: ) -def test_hash() -> None: - multipoint = geometry.MultiPoint([(0, 0), (1, 0), (2, 2)]) - - assert hash(multipoint) == hash(geometry.MultiPoint([(0, 0), (1, 0), (2, 2)])) - - def test_empty() -> None: multipoint = geometry.MultiPoint([(1, None)]) diff --git a/tests/test_multipolygon.py b/tests/test_multipolygon.py index 4ae637ba..967b8fee 100644 --- a/tests/test_multipolygon.py +++ b/tests/test_multipolygon.py @@ -284,41 +284,3 @@ def test_empty_bounds() -> None: polys = geometry.MultiPolygon([]) assert polys.bounds == () - - -def test_hash() -> None: - polys = geometry.MultiPolygon( - ( - ( - ((0, 0), (0, 2), (2, 2), (2, 0), (0, 0)), - ( - ((1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)), - ((1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)), - ), - ), - (((0, 0, 0), (1, 1, 0), (1, 0, 0), (0, 0, 0)),), - ( - ((0, 0), (0, 2), (2, 2), (2, 0), (0, 0)), - (((1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)),), - ), - ), - ) - - assert hash(polys) == hash( - geometry.MultiPolygon( - ( - ( - ((0, 0), (0, 2), (2, 2), (2, 0), (0, 0)), - ( - ((1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)), - ((1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)), - ), - ), - (((0, 0, 0), (1, 1, 0), (1, 0, 0), (0, 0, 0)),), - ( - ((0, 0), (0, 2), (2, 2), (2, 0), (0, 0)), - (((1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)),), - ), - ), - ), - ) diff --git a/tests/test_point.py b/tests/test_point.py index 37ad8803..33a3fb93 100644 --- a/tests/test_point.py +++ b/tests/test_point.py @@ -57,12 +57,6 @@ def test_xy() -> None: assert point.y == 0 -def test_hash() -> None: - point = geometry.Point(1.0, 2.0, 3.0) - - assert hash(point) == hash((1.0, 2.0, 3.0)) - - def test_xy_raises_error_accessing_z() -> None: point = geometry.Point(1, 0) @@ -244,9 +238,3 @@ def test_empty_bounds() -> None: point = geometry.Point(None, None) assert point.bounds == () - - -def test_hash_empty() -> None: - point = geometry.Point(None, None) - - assert hash(point) == hash(()) diff --git a/tests/test_polygon.py b/tests/test_polygon.py index 07c8b056..4ef462d3 100644 --- a/tests/test_polygon.py +++ b/tests/test_polygon.py @@ -20,12 +20,6 @@ def test_coords_with_holes() -> None: ) -def test_hash() -> None: - polygon = geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]) - - assert hash(polygon) == hash(geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)])) - - def test_geo_interface_shell_only() -> None: polygon = geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]) From 6b90f3f3159ddaad7ee0f259e0775f992cc5cca2 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sun, 26 Nov 2023 14:27:14 +0000 Subject: [PATCH 36/41] Add coverage configuration to pyproject.toml --- pyproject.toml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 78a35d36..03c38324 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,6 +95,15 @@ Changelog = "https://github.com/cleder/pygeoif/blob/develop/docs/HISTORY.rst" Documentation = "https://pygeoif.readthedocs.io/" Homepage = "https://github.com/cleder/pygeoif/" +[tool.coverage.paths] +source = [ + "pygeoif", + "tests", +] + +[tool.coverage.run] +branch = true + [tool.flake8] max_line_length = 88 From c4ede9e19536edc8a90fafcca4feb0473b721f03 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sun, 26 Nov 2023 14:36:40 +0000 Subject: [PATCH 37/41] Add test for empty points omission in LineString --- tests/test_line.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_line.py b/tests/test_line.py index b0b4df8a..6cc60f40 100644 --- a/tests/test_line.py +++ b/tests/test_line.py @@ -19,6 +19,12 @@ def test_coords_get_3d() -> None: assert line.coords == ((0.0, 0.0, 0), (1.0, 1.0, 1)) +def test_empty_points_omitted() -> None: + line = geometry.LineString([(0, 0, 0), (None, None, None), (2, 2, 2)]) + + assert line.coords == ((0, 0, 0), (2, 2, 2)) + + def test_set_geoms_raises() -> None: line = geometry.LineString([(0, 0), (1, 0)]) # pragma: no mutate From 9a94a7f30af4b6e36ff0aac925491bceedbfa1d2 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Mon, 27 Nov 2023 13:07:53 +0000 Subject: [PATCH 38/41] Add slotscheck hook, include py.typed --- .pre-commit-config.yaml | 4 ++++ pyproject.toml | 1 + 2 files changed, 5 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b5a3bbcd..b3b6eaf5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,6 +31,10 @@ repos: rev: v0.3.1 hooks: - id: absolufy-imports + - repo: https://github.com/ariebovenberg/slotscheck + rev: v0.17.1 + hooks: + - id: slotscheck - repo: https://github.com/pycqa/isort rev: 5.12.0 hooks: diff --git a/pyproject.toml b/pyproject.toml index 03c38324..7a3bee81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -261,4 +261,5 @@ exclude = [ ] include = [ "pygeoif*", + "pygeoif/py.typed", ] From c19305686c7bdf8e8636755b223dae982c8316b1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 27 Nov 2023 16:34:39 +0000 Subject: [PATCH 39/41] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.7.0 → v1.7.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.7.0...v1.7.1) - [github.com/python-jsonschema/check-jsonschema: 0.27.1 → 0.27.2](https://github.com/python-jsonschema/check-jsonschema/compare/0.27.1...0.27.2) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b3b6eaf5..7d5a794e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -72,7 +72,7 @@ repos: - flake8-typing-imports - flake8-use-fstring - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.7.0 + rev: v1.7.1 hooks: - id: mypy # - repo: https://github.com/Lucas-C/pre-commit-hooks-markup @@ -88,7 +88,7 @@ repos: hooks: - id: pyprojectsort - repo: https://github.com/python-jsonschema/check-jsonschema - rev: "0.27.1" + rev: "0.27.2" hooks: - id: check-github-workflows - id: check-github-actions From 76e08fe0be93e96f77e5fe49dfdd6a96e4ef7407 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Mon, 27 Nov 2023 17:14:58 +0000 Subject: [PATCH 40/41] Remove slotscheck hook from pre-commit-config.yaml --- .pre-commit-config.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b3b6eaf5..b5a3bbcd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,10 +31,6 @@ repos: rev: v0.3.1 hooks: - id: absolufy-imports - - repo: https://github.com/ariebovenberg/slotscheck - rev: v0.17.1 - hooks: - - id: slotscheck - repo: https://github.com/pycqa/isort rev: 5.12.0 hooks: From 11b9410aabe6022dd6cf992d3eee7af0caddf63f Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Mon, 27 Nov 2023 17:25:59 +0000 Subject: [PATCH 41/41] Release notes 1.2.0 --- docs/HISTORY.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/HISTORY.rst b/docs/HISTORY.rst index 2ccdf5fd..6e5c9718 100644 --- a/docs/HISTORY.rst +++ b/docs/HISTORY.rst @@ -1,7 +1,7 @@ Changelog ========= -1.2.0 (unreleased) +1.2.0 (2023/11/27) ------------------ - remove Python 3.7 support