Skip to content

Commit 7af53bf

Browse files
Merge pull request #100 from eseglem/feature/allow-empty-arrays
Allow empty coordinate arrays
2 parents 49af995 + 40726e2 commit 7af53bf

File tree

3 files changed

+78
-19
lines changed

3 files changed

+78
-19
lines changed

geojson_pydantic/geometries.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,9 +159,9 @@ def check_closure(cls, coordinates: List) -> List:
159159
return coordinates
160160

161161
@property
162-
def exterior(self) -> LinearRing:
162+
def exterior(self) -> Union[LinearRing, None]:
163163
"""Return the exterior Linear Ring of the polygon."""
164-
return self.coordinates[0]
164+
return self.coordinates[0] if self.coordinates else None
165165

166166
@property
167167
def interiors(self) -> Iterator[LinearRing]:
@@ -207,6 +207,14 @@ def _wkt_coordinates(self) -> str:
207207
f"({_lines_wtk_coordinates(polygon)})" for polygon in self.coordinates
208208
)
209209

210+
@validator("coordinates")
211+
def check_closure(cls, coordinates: List) -> List:
212+
"""Validate that Polygon is closed (first and last coordinate are the same)."""
213+
if any([ring[-1] != ring[0] for polygon in coordinates for ring in polygon]):
214+
raise ValueError("All linear rings have the same start and end coordinates")
215+
216+
return coordinates
217+
210218

211219
Geometry = Union[Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon]
212220

@@ -242,7 +250,11 @@ def _wkt_coordinates(self) -> str:
242250
@property
243251
def wkt(self) -> str:
244252
"""Return the Well Known Text representation."""
245-
return f"{self._wkt_type} ({self._wkt_coordinates})"
253+
return (
254+
self._wkt_type
255+
+ " "
256+
+ (f"({self._wkt_coordinates})" if self._wkt_coordinates else "EMPTY")
257+
)
246258

247259
@property
248260
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)
@@ -438,3 +470,22 @@ class PointType(Point):
438470
PointType(type="Point", coordinates=(1.01, 2.01)).wkt
439471
== Point(type="Point", coordinates=(1.01, 2.01)).wkt
440472
)
473+
474+
475+
@pytest.mark.parametrize(
476+
"shape",
477+
[
478+
MultiPoint,
479+
MultiLineString,
480+
Polygon,
481+
MultiPolygon,
482+
],
483+
)
484+
def test_wkt_empty(shape):
485+
assert shape(type=shape.__name__, coordinates=[]).wkt.endswith(" EMPTY")
486+
487+
488+
def test_wkt_empty_geometrycollection():
489+
assert GeometryCollection(type="GeometryCollection", geometries=[]).wkt.endswith(
490+
" EMPTY"
491+
)

0 commit comments

Comments
 (0)