Skip to content

Commit 8a2e2c6

Browse files
committed
Merge branch 'master' into bugfix/102-wkt-mixed-dimensionality
2 parents 691a1ad + 46c088d commit 8a2e2c6

File tree

5 files changed

+35
-76
lines changed

5 files changed

+35
-76
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
2727
```
2828

2929
- Add has_z function to Geometries (author @eseglem, https://github.com/developmentseed/geojson-pydantic/pull/103)
30+
- Add optional bbox to geometries.
3031

3132
### Changed
3233

geojson_pydantic/features.py

Lines changed: 3 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@
55
from pydantic import BaseModel, Field, validator
66
from pydantic.generics import GenericModel
77

8+
from geojson_pydantic.geo_interface import GeoInterfaceMixin
89
from geojson_pydantic.geometries import Geometry, GeometryCollection
910
from geojson_pydantic.types import BBox
1011

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

1415

15-
class Feature(GenericModel, Generic[Geom, Props]):
16+
class Feature(GenericModel, Generic[Geom, Props], GeoInterfaceMixin):
1617
"""Feature Model"""
1718

1819
type: Literal["Feature"]
@@ -34,30 +35,8 @@ def set_geometry(cls, geometry: Any) -> Any:
3435

3536
return geometry
3637

37-
@property
38-
def __geo_interface__(self) -> Dict[str, Any]:
39-
"""GeoJSON-like protocol for geo-spatial (GIS) vector data.
4038

41-
ref: https://gist.github.com/sgillies/2217756#__geo_interface
42-
"""
43-
geo: Dict[str, Any] = {
44-
"type": self.type,
45-
"geometry": self.geometry.__geo_interface__
46-
if self.geometry is not None
47-
else None,
48-
"properties": self.properties,
49-
}
50-
51-
if self.bbox:
52-
geo["bbox"] = self.bbox
53-
54-
if self.id:
55-
geo["id"] = self.id
56-
57-
return geo
58-
59-
60-
class FeatureCollection(GenericModel, Generic[Geom, Props]):
39+
class FeatureCollection(GenericModel, Generic[Geom, Props], GeoInterfaceMixin):
6140
"""FeatureCollection Model"""
6241

6342
type: Literal["FeatureCollection"]
@@ -75,19 +54,3 @@ def __len__(self) -> int:
7554
def __getitem__(self, index: int) -> Feature:
7655
"""get feature at a given index"""
7756
return self.features[index]
78-
79-
@property
80-
def __geo_interface__(self) -> Dict[str, Any]:
81-
"""GeoJSON-like protocol for geo-spatial (GIS) vector data.
82-
83-
ref: https://gist.github.com/sgillies/2217756#__geo_interface
84-
"""
85-
features: List[Dict[str, Any]] = []
86-
for feat in self.features:
87-
features.append(feat.__geo_interface__)
88-
89-
geo: Dict[str, Any] = {"type": self.type, "features": features}
90-
if self.bbox:
91-
geo["bbox"] = self.bbox
92-
93-
return geo

geojson_pydantic/geo_interface.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""Mixin for __geo_interface__ on GeoJSON objects."""
2+
3+
from typing import Any, Dict, Protocol
4+
5+
6+
class _DictProtocol(Protocol):
7+
"""Protocol for use as the type of self in the mixin."""
8+
9+
def dict(self, *, exclude_unset: bool, **args: Any) -> Dict[str, Any]:
10+
"""Define a dict function so the mixin knows it exists."""
11+
...
12+
13+
14+
class GeoInterfaceMixin:
15+
"""Mixin for __geo_interface__ on GeoJSON objects."""
16+
17+
@property
18+
def __geo_interface__(self: _DictProtocol) -> Dict[str, Any]:
19+
"""GeoJSON-like protocol for geo-spatial (GIS) vector data.
20+
21+
ref: https://gist.github.com/sgillies/2217756#__geo_interface
22+
"""
23+
return self.dict(exclude_unset=True)

geojson_pydantic/geometries.py

Lines changed: 7 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
"""pydantic models for GeoJSON Geometry objects."""
22
import abc
3-
from typing import Any, Dict, Iterator, List, Literal, Protocol, Union
3+
from typing import Any, Iterator, List, Literal, Optional, Protocol, Union
44

55
from pydantic import BaseModel, Field, ValidationError, validator
66
from pydantic.error_wrappers import ErrorWrapper
77
from typing_extensions import Annotated
88

9+
from geojson_pydantic.geo_interface import GeoInterfaceMixin
910
from geojson_pydantic.types import (
11+
BBox,
1012
LinearRing,
1113
LineStringCoords,
1214
MultiLineStringCoords,
@@ -72,22 +74,15 @@ def __call__(self, coordinates: Any, force_z: bool) -> str:
7274
...
7375

7476

75-
class _GeometryBase(BaseModel, abc.ABC):
77+
class _GeometryBase(BaseModel, abc.ABC, GeoInterfaceMixin):
7678
"""Base class for geometry models"""
7779

7880
type: str
7981
coordinates: Any
82+
bbox: Optional[BBox] = None
8083

8184
__wkt_coordinates__: _WktCallable
8285

83-
@property
84-
def __geo_interface__(self) -> Dict[str, Any]:
85-
"""GeoJSON-like protocol for geo-spatial (GIS) vector data.
86-
87-
ref: https://gist.github.com/sgillies/2217756#__geo_interface
88-
"""
89-
return {"type": self.type, "coordinates": self.coordinates}
90-
9186
@property
9287
@abc.abstractmethod
9388
def has_z(self) -> bool:
@@ -168,18 +163,6 @@ def has_z(self) -> bool:
168163
return _lines_has_z(self.coordinates)
169164

170165

171-
class LinearRingGeom(LineString):
172-
"""LinearRing model."""
173-
174-
@validator("coordinates")
175-
def check_closure(cls, coordinates: List) -> List:
176-
"""Validate that LinearRing is closed (first and last coordinate are the same)."""
177-
if coordinates[-1] != coordinates[0]:
178-
raise ValueError("LinearRing must have the same start and end coordinates")
179-
180-
return coordinates
181-
182-
183166
class Polygon(_GeometryBase):
184167
"""Polygon Model"""
185168

@@ -254,11 +237,12 @@ def check_closure(cls, coordinates: List) -> List:
254237
]
255238

256239

257-
class GeometryCollection(BaseModel):
240+
class GeometryCollection(BaseModel, GeoInterfaceMixin):
258241
"""GeometryCollection Model"""
259242

260243
type: Literal["GeometryCollection"]
261244
geometries: List[Geometry]
245+
bbox: Optional[BBox] = None
262246

263247
def __iter__(self) -> Iterator[Geometry]: # type: ignore [override]
264248
"""iterate over geometries"""
@@ -290,18 +274,6 @@ def wkt(self) -> str:
290274
z = " Z " if "Z" in geometries else " "
291275
return f"{self.type.upper()}{z}{geometries}"
292276

293-
@property
294-
def __geo_interface__(self) -> Dict[str, Any]:
295-
"""GeoJSON-like protocol for geo-spatial (GIS) vector data.
296-
297-
ref: https://gist.github.com/sgillies/2217756#__geo_interface
298-
"""
299-
geometries: List[Dict[str, Any]] = []
300-
for geom in self.geometries:
301-
geometries.append(geom.__geo_interface__)
302-
303-
return {"type": self.type, "geometries": self.geometries}
304-
305277

306278
def parse_geometry_obj(obj: Any) -> Geometry:
307279
"""

tests/test_features.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ class Pointy:
168168
__geo_interface__ = {"type": "Point", "coordinates": (0.0, 0.0)}
169169

170170
feat = Feature(type="Feature", geometry=Pointy(), properties={})
171-
assert feat.geometry.dict() == Pointy.__geo_interface__
171+
assert feat.geometry.dict(exclude_unset=True) == Pointy.__geo_interface__
172172

173173

174174
def test_feature_with_null_geometry():

0 commit comments

Comments
 (0)