Skip to content

Commit 6c40701

Browse files
committed
merge
2 parents ffe27f5 + b93b2b6 commit 6c40701

File tree

3 files changed

+84
-21
lines changed

3 files changed

+84
-21
lines changed

geojson_pydantic/geometries.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
import abc
44
from typing import Any, Dict, Iterator, List, Literal, Union
55

6-
from pydantic import BaseModel, ValidationError, validator
6+
from pydantic import BaseModel, Field, ValidationError, validator
77
from pydantic.error_wrappers import ErrorWrapper
8+
from typing_extensions import Annotated
89

910
from geojson_pydantic.types import (
1011
LinearRing,
@@ -186,9 +187,9 @@ def check_closure(cls, coordinates: List) -> List:
186187
return coordinates
187188

188189
@property
189-
def exterior(self) -> LinearRing:
190+
def exterior(self) -> Union[LinearRing, None]:
190191
"""Return the exterior Linear Ring of the polygon."""
191-
return self.coordinates[0]
192+
return self.coordinates[0] if self.coordinates else None
192193

193194
@property
194195
def interiors(self) -> Iterator[LinearRing]:
@@ -236,8 +237,19 @@ def _wkt_coordinates(self) -> str:
236237
f"({_lines_wtk_coordinates(polygon)})" for polygon in self.coordinates
237238
)
238239

240+
@validator("coordinates")
241+
def check_closure(cls, coordinates: List) -> List:
242+
"""Validate that Polygon is closed (first and last coordinate are the same)."""
243+
if any([ring[-1] != ring[0] for polygon in coordinates for ring in polygon]):
244+
raise ValueError("All linear rings have the same start and end coordinates")
245+
246+
return coordinates
239247

240-
Geometry = Union[Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon]
248+
249+
Geometry = Annotated[
250+
Union[Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon],
251+
Field(discriminator="type"),
252+
]
241253

242254

243255
class GeometryCollection(BaseModel):
@@ -271,7 +283,11 @@ def _wkt_coordinates(self) -> str:
271283
@property
272284
def wkt(self) -> str:
273285
"""Return the Well Known Text representation."""
274-
return f"{self._wkt_type} ({self._wkt_coordinates})"
286+
return (
287+
self._wkt_type
288+
+ " "
289+
+ (f"({self._wkt_coordinates})" if self._wkt_coordinates else "EMPTY")
290+
)
275291

276292
@property
277293
def __geo_interface__(self) -> Dict[str, Any]:

geojson_pydantic/types.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,14 @@
1111
Position = Union[Tuple[float, float], Tuple[float, float, float]]
1212

1313
# Coordinate arrays
14-
1514
if TYPE_CHECKING:
16-
MultiPointCoords = List[Position]
1715
LineStringCoords = List[Position]
18-
MultiLineStringCoords = List[List[Position]]
1916
LinearRing = List[Position]
20-
PolygonCoords = List[List[Position]]
21-
MultiPolygonCoords = List[List[List[Position]]]
2217
else:
23-
MultiPointCoords = conlist(Position, min_items=1)
2418
LineStringCoords = conlist(Position, min_items=2)
25-
MultiLineStringCoords = conlist(LineStringCoords, min_items=1)
2619
LinearRing = conlist(Position, min_items=4)
27-
PolygonCoords = conlist(LinearRing, min_items=1)
28-
MultiPolygonCoords = conlist(PolygonCoords, min_items=1)
20+
21+
MultiPointCoords = List[Position]
22+
MultiLineStringCoords = List[LineStringCoords]
23+
PolygonCoords = List[LinearRing]
24+
MultiPolygonCoords = List[PolygonCoords]

tests/test_geometries.py

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def assert_wkt_equivalence(geom: Union[Geometry, GeometryCollection]):
2828
@pytest.mark.parametrize("coordinates", [(1.01, 2.01), (1.0, 2.0, 3.0), (1.0, 2.0)])
2929
def test_point_valid_coordinates(coordinates):
3030
"""
31-
Two or three number elements as coordinates shold be okay
31+
Two or three number elements as coordinates should be okay
3232
"""
3333
p = Point(type="Point", coordinates=coordinates)
3434
assert p.type == "Point"
@@ -38,7 +38,8 @@ def test_point_valid_coordinates(coordinates):
3838

3939

4040
@pytest.mark.parametrize(
41-
"coordinates", [(1.0,), (1.0, 2.0, 3.0, 4.0), "Foo", (None, 2.0), (1.0, (2.0,))]
41+
"coordinates",
42+
[(1.0,), (1.0, 2.0, 3.0, 4.0), "Foo", (None, 2.0), (1.0, (2.0,)), (), [], None],
4243
)
4344
def test_point_invalid_coordinates(coordinates):
4445
"""
@@ -51,6 +52,8 @@ def test_point_invalid_coordinates(coordinates):
5152
@pytest.mark.parametrize(
5253
"coordinates",
5354
[
55+
# Empty array
56+
[],
5457
# No Z
5558
[(1.0, 2.0)],
5659
[(1.0, 2.0), (1.0, 2.0)],
@@ -60,7 +63,7 @@ def test_point_invalid_coordinates(coordinates):
6063
)
6164
def test_multi_point_valid_coordinates(coordinates):
6265
"""
63-
Two or three number elements as coordinates shold be okay
66+
Two or three number elements as coordinates should be okay, as well as an empty array.
6467
"""
6568
p = MultiPoint(type="MultiPoint", coordinates=coordinates)
6669
assert p.type == "MultiPoint"
@@ -71,7 +74,7 @@ def test_multi_point_valid_coordinates(coordinates):
7174

7275
@pytest.mark.parametrize(
7376
"coordinates",
74-
[[(1.0,)], [(1.0, 2.0, 3.0, 4.0)], ["Foo"], [(None, 2.0)], [(1.0, (2.0,))]],
77+
[[(1.0,)], [(1.0, 2.0, 3.0, 4.0)], ["Foo"], [(None, 2.0)], [(1.0, (2.0,))], None],
7578
)
7679
def test_multi_point_invalid_coordinates(coordinates):
7780
"""
@@ -115,6 +118,8 @@ def test_line_string_invalid_coordinates(coordinates):
115118
@pytest.mark.parametrize(
116119
"coordinates",
117120
[
121+
# Empty array
122+
[],
118123
# One line, two points, no Z
119124
[[(1.0, 2.0), (3.0, 4.0)]],
120125
# One line, two points, has Z
@@ -139,7 +144,7 @@ def test_multi_line_string_valid_coordinates(coordinates):
139144

140145

141146
@pytest.mark.parametrize(
142-
"coordinates", [[None], ["Foo"], [[]], [[(1.0, 2.0)]], [["Foo", "Bar"]]]
147+
"coordinates", [None, [None], ["Foo"], [[]], [[(1.0, 2.0)]], [["Foo", "Bar"]]]
143148
)
144149
def test_multi_line_string_invalid_coordinates(coordinates):
145150
"""
@@ -152,6 +157,8 @@ def test_multi_line_string_invalid_coordinates(coordinates):
152157
@pytest.mark.parametrize(
153158
"coordinates",
154159
[
160+
# Empty array
161+
[],
155162
# Polygon, no Z
156163
[[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]],
157164
# Polygon, has Z
@@ -166,7 +173,10 @@ def test_polygon_valid_coordinates(coordinates):
166173
assert polygon.type == "Polygon"
167174
assert polygon.coordinates == coordinates
168175
assert hasattr(polygon, "__geo_interface__")
169-
assert polygon.exterior == coordinates[0]
176+
if polygon.coordinates:
177+
assert polygon.exterior == coordinates[0]
178+
else:
179+
assert polygon.exterior is None
170180
assert not list(polygon.interiors)
171181
assert_wkt_equivalence(polygon)
172182

@@ -212,10 +222,10 @@ def test_polygon_with_holes(coordinates):
212222
"coordinates",
213223
[
214224
"foo",
225+
None,
215226
[[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)], "foo", None],
216227
[[(1.0, 2.0), (3.0, 4.0), (1.0, 2.0)]],
217228
[[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (7.0, 8.0)]],
218-
[],
219229
],
220230
)
221231
def test_polygon_invalid_coordinates(coordinates):
@@ -233,6 +243,8 @@ def test_polygon_invalid_coordinates(coordinates):
233243
@pytest.mark.parametrize(
234244
"coordinates",
235245
[
246+
# Empty array
247+
[],
236248
# Multipolygon, no Z
237249
[
238250
[
@@ -270,6 +282,26 @@ def test_multi_polygon(coordinates):
270282
assert_wkt_equivalence(multi_polygon)
271283

272284

285+
@pytest.mark.parametrize(
286+
"coordinates",
287+
[
288+
"foo",
289+
None,
290+
[
291+
[
292+
[(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (0.0, 0.0)],
293+
],
294+
[
295+
[(2.1, 2.1), (2.2, 2.1), (2.2, 2.2), (2.1, 4.2)],
296+
],
297+
],
298+
],
299+
)
300+
def test_multipolygon_invalid_coordinates(coordinates):
301+
with pytest.raises(ValidationError):
302+
MultiPolygon(type="MultiPolygon", coordinates=coordinates)
303+
304+
273305
def test_parse_geometry_obj_point():
274306
assert parse_geometry_obj({"type": "Point", "coordinates": [102.0, 0.5]}) == Point(
275307
type="Point", coordinates=(102.0, 0.5)
@@ -557,3 +589,22 @@ def test_polygon_has_z(coordinates, expected):
557589
)
558590
def test_multipolygon_has_z(coordinates, expected):
559591
assert MultiPolygon(type="MultiPolygon", coordinates=coordinates).has_z == expected
592+
593+
594+
@pytest.mark.parametrize(
595+
"shape",
596+
[
597+
MultiPoint,
598+
MultiLineString,
599+
Polygon,
600+
MultiPolygon,
601+
],
602+
)
603+
def test_wkt_empty(shape):
604+
assert shape(type=shape.__name__, coordinates=[]).wkt.endswith(" EMPTY")
605+
606+
607+
def test_wkt_empty_geometrycollection():
608+
assert GeometryCollection(type="GeometryCollection", geometries=[]).wkt.endswith(
609+
" EMPTY"
610+
)

0 commit comments

Comments
 (0)