Skip to content

Commit

Permalink
Tweak Geometry to include GeometryCollection. Fixes: #93
Browse files Browse the repository at this point in the history
  • Loading branch information
eseglem committed Jul 5, 2023
1 parent 40dbafb commit 8033f87
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 20 deletions.
4 changes: 2 additions & 2 deletions geojson_pydantic/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
from pydantic import BaseModel, Field, StrictInt, StrictStr, field_validator

from geojson_pydantic.geo_interface import GeoInterfaceMixin
from geojson_pydantic.geometries import Geometry, GeometryCollection
from geojson_pydantic.geometries import Geometry
from geojson_pydantic.types import BBox, validate_bbox

Props = TypeVar("Props", bound=Union[Dict[str, Any], BaseModel])
Geom = TypeVar("Geom", bound=Union[Geometry, GeometryCollection])
Geom = TypeVar("Geom", bound=Geometry)


class Feature(BaseModel, Generic[Geom, Props], GeoInterfaceMixin):
Expand Down
29 changes: 20 additions & 9 deletions geojson_pydantic/geometries.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,28 +246,22 @@ def check_closure(cls, coordinates: List) -> List:
return coordinates


Geometry = Annotated[
Union[Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon],
Field(discriminator="type"),
]


class GeometryCollection(BaseModel, GeoInterfaceMixin):
"""GeometryCollection Model"""

type: Literal["GeometryCollection"]
geometries: List[Union[Geometry, GeometryCollection]]
geometries: List[Geometry]
bbox: Optional[BBox] = None

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

def __len__(self) -> int:
"""return geometries length"""
return len(self.geometries)

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

Expand Down Expand Up @@ -312,6 +306,20 @@ def check_geometries(cls, geometries: List) -> List:
return geometries


Geometry = Annotated[
Union[
Point,
MultiPoint,
LineString,
MultiLineString,
Polygon,
MultiPolygon,
GeometryCollection,
],
Field(discriminator="type"),
]


def parse_geometry_obj(obj: Any) -> Geometry:
"""
`obj` is an object that is supposed to represent a GeoJSON geometry. This method returns the
Expand All @@ -338,4 +346,7 @@ def parse_geometry_obj(obj: Any) -> Geometry:
elif obj["type"] == "MultiPolygon":
return MultiPolygon.parse_obj(obj)

elif obj["type"] == "GeometryCollection":
return GeometryCollection.model_validate(obj)

raise ValueError(f"Unknown type: {obj['type']}")
14 changes: 5 additions & 9 deletions geojson_pydantic/types.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""Types for geojson_pydantic models"""

from typing import TYPE_CHECKING, List, Optional, Tuple, TypeVar, Union
from typing import List, Optional, Tuple, TypeVar, Union

from pydantic import conlist
from pydantic import Field
from typing_extensions import Annotated

T = TypeVar("T")

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

# Coordinate arrays
if TYPE_CHECKING:
LineStringCoords = List[Position]
LinearRing = List[Position]
else:
LineStringCoords = conlist(Position, min_length=2)
LinearRing = conlist(Position, min_length=4)

LineStringCoords = Annotated[List[Position], Field(min_length=2)]
LinearRing = Annotated[List[Position], Field(min_length=4)]
MultiPointCoords = List[Position]
MultiLineStringCoords = List[LineStringCoords]
PolygonCoords = List[LinearRing]
Expand Down
27 changes: 27 additions & 0 deletions tests/test_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,30 @@ def test_bbox_validation():
bbox=(0, "a", 0, 1, 1, 1),
geometry=None,
)


def test_feature_validation_error_count():
# Tests that validation does not include irrelevant errors to make them
# easier to read. The input below used to raise 18 errors.
# See #93 for more details.
with pytest.raises(ValidationError):
try:
Feature(
type="Feature",
geometry=Polygon(
type="Polygon",
coordinates=[
[
(-55.9947406591177, -9.26104045526505),
(-55.9976752102375, -9.266589696568962),
(-56.00200328975916, -9.264041751931352),
(-55.99899921566248, -9.257935213034594),
(-55.99477406591177, -9.26103945526505),
]
],
),
properties={},
)
except ValidationError as e:
assert e.error_count() == 1
raise
18 changes: 18 additions & 0 deletions tests/test_geometries.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,24 @@ def test_parse_geometry_obj_multi_polygon():
)


def test_parse_geometry_obj_geometry_collection():
assert parse_geometry_obj(
{
"type": "GeometryCollection",
"geometries": [
{"type": "Point", "coordinates": [102.0, 0.5]},
{"type": "MultiPoint", "coordinates": [[100.0, 0.0], [101.0, 1.0]]},
],
}
) == GeometryCollection(
type="GeometryCollection",
geometries=[
Point(type="Point", coordinates=(102.0, 0.5)),
MultiPoint(type="MultiPoint", coordinates=[(100.0, 0.0), (101.0, 1.0)]),
],
)


def test_parse_geometry_obj_invalid_type():
with pytest.raises(ValueError):
parse_geometry_obj({"type": "This type", "obviously": "doesn't exist"})
Expand Down

0 comments on commit 8033f87

Please sign in to comment.