Skip to content

Commit 8033f87

Browse files
committed
Tweak Geometry to include GeometryCollection. Fixes: #93
1 parent 40dbafb commit 8033f87

File tree

5 files changed

+72
-20
lines changed

5 files changed

+72
-20
lines changed

geojson_pydantic/features.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
from pydantic import BaseModel, Field, StrictInt, StrictStr, field_validator
66

77
from geojson_pydantic.geo_interface import GeoInterfaceMixin
8-
from geojson_pydantic.geometries import Geometry, GeometryCollection
8+
from geojson_pydantic.geometries import Geometry
99
from geojson_pydantic.types import BBox, validate_bbox
1010

1111
Props = TypeVar("Props", bound=Union[Dict[str, Any], BaseModel])
12-
Geom = TypeVar("Geom", bound=Union[Geometry, GeometryCollection])
12+
Geom = TypeVar("Geom", bound=Geometry)
1313

1414

1515
class Feature(BaseModel, Generic[Geom, Props], GeoInterfaceMixin):

geojson_pydantic/geometries.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -246,28 +246,22 @@ def check_closure(cls, coordinates: List) -> List:
246246
return coordinates
247247

248248

249-
Geometry = Annotated[
250-
Union[Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon],
251-
Field(discriminator="type"),
252-
]
253-
254-
255249
class GeometryCollection(BaseModel, GeoInterfaceMixin):
256250
"""GeometryCollection Model"""
257251

258252
type: Literal["GeometryCollection"]
259-
geometries: List[Union[Geometry, GeometryCollection]]
253+
geometries: List[Geometry]
260254
bbox: Optional[BBox] = None
261255

262-
def __iter__(self) -> Iterator[Union[Geometry, GeometryCollection]]: # type: ignore [override]
256+
def __iter__(self) -> Iterator[Geometry]: # type: ignore [override]
263257
"""iterate over geometries"""
264258
return iter(self.geometries)
265259

266260
def __len__(self) -> int:
267261
"""return geometries length"""
268262
return len(self.geometries)
269263

270-
def __getitem__(self, index: int) -> Union[Geometry, GeometryCollection]:
264+
def __getitem__(self, index: int) -> Geometry:
271265
"""get geometry at a given index"""
272266
return self.geometries[index]
273267

@@ -312,6 +306,20 @@ def check_geometries(cls, geometries: List) -> List:
312306
return geometries
313307

314308

309+
Geometry = Annotated[
310+
Union[
311+
Point,
312+
MultiPoint,
313+
LineString,
314+
MultiLineString,
315+
Polygon,
316+
MultiPolygon,
317+
GeometryCollection,
318+
],
319+
Field(discriminator="type"),
320+
]
321+
322+
315323
def parse_geometry_obj(obj: Any) -> Geometry:
316324
"""
317325
`obj` is an object that is supposed to represent a GeoJSON geometry. This method returns the
@@ -338,4 +346,7 @@ def parse_geometry_obj(obj: Any) -> Geometry:
338346
elif obj["type"] == "MultiPolygon":
339347
return MultiPolygon.parse_obj(obj)
340348

349+
elif obj["type"] == "GeometryCollection":
350+
return GeometryCollection.model_validate(obj)
351+
341352
raise ValueError(f"Unknown type: {obj['type']}")

geojson_pydantic/types.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
"""Types for geojson_pydantic models"""
22

3-
from typing import TYPE_CHECKING, List, Optional, Tuple, TypeVar, Union
3+
from typing import List, Optional, Tuple, TypeVar, Union
44

5-
from pydantic import conlist
5+
from pydantic import Field
6+
from typing_extensions import Annotated
67

78
T = TypeVar("T")
89

@@ -44,13 +45,8 @@ def validate_bbox(bbox: Optional[BBox]) -> Optional[BBox]:
4445
Position = Union[Tuple[float, float], Tuple[float, float, float]]
4546

4647
# Coordinate arrays
47-
if TYPE_CHECKING:
48-
LineStringCoords = List[Position]
49-
LinearRing = List[Position]
50-
else:
51-
LineStringCoords = conlist(Position, min_length=2)
52-
LinearRing = conlist(Position, min_length=4)
53-
48+
LineStringCoords = Annotated[List[Position], Field(min_length=2)]
49+
LinearRing = Annotated[List[Position], Field(min_length=4)]
5450
MultiPointCoords = List[Position]
5551
MultiLineStringCoords = List[LineStringCoords]
5652
PolygonCoords = List[LinearRing]

tests/test_features.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,3 +263,30 @@ def test_bbox_validation():
263263
bbox=(0, "a", 0, 1, 1, 1),
264264
geometry=None,
265265
)
266+
267+
268+
def test_feature_validation_error_count():
269+
# Tests that validation does not include irrelevant errors to make them
270+
# easier to read. The input below used to raise 18 errors.
271+
# See #93 for more details.
272+
with pytest.raises(ValidationError):
273+
try:
274+
Feature(
275+
type="Feature",
276+
geometry=Polygon(
277+
type="Polygon",
278+
coordinates=[
279+
[
280+
(-55.9947406591177, -9.26104045526505),
281+
(-55.9976752102375, -9.266589696568962),
282+
(-56.00200328975916, -9.264041751931352),
283+
(-55.99899921566248, -9.257935213034594),
284+
(-55.99477406591177, -9.26103945526505),
285+
]
286+
],
287+
),
288+
properties={},
289+
)
290+
except ValidationError as e:
291+
assert e.error_count() == 1
292+
raise

tests/test_geometries.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,24 @@ def test_parse_geometry_obj_multi_polygon():
427427
)
428428

429429

430+
def test_parse_geometry_obj_geometry_collection():
431+
assert parse_geometry_obj(
432+
{
433+
"type": "GeometryCollection",
434+
"geometries": [
435+
{"type": "Point", "coordinates": [102.0, 0.5]},
436+
{"type": "MultiPoint", "coordinates": [[100.0, 0.0], [101.0, 1.0]]},
437+
],
438+
}
439+
) == GeometryCollection(
440+
type="GeometryCollection",
441+
geometries=[
442+
Point(type="Point", coordinates=(102.0, 0.5)),
443+
MultiPoint(type="MultiPoint", coordinates=[(100.0, 0.0), (101.0, 1.0)]),
444+
],
445+
)
446+
447+
430448
def test_parse_geometry_obj_invalid_type():
431449
with pytest.raises(ValueError):
432450
parse_geometry_obj({"type": "This type", "obviously": "doesn't exist"})

0 commit comments

Comments
 (0)