Skip to content

Commit 5f98bdb

Browse files
committed
enforce type, geometry and properties
1 parent 1ff7b66 commit 5f98bdb

File tree

6 files changed

+80
-60
lines changed

6 files changed

+80
-60
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
runs-on: ubuntu-latest
1616
strategy:
1717
matrix:
18-
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11']
18+
python-version: ['3.8', '3.9', '3.10', '3.11']
1919

2020
steps:
2121
- uses: actions/checkout@v3

geojson_pydantic/features.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""pydantic models for GeoJSON Feature objects."""
22

3-
from typing import Any, Dict, Generic, Iterator, List, Optional, TypeVar, Union
3+
from typing import Any, Dict, Generic, Iterator, List, Literal, Optional, TypeVar, Union
44

55
from pydantic import BaseModel, Field, validator
66
from pydantic.generics import GenericModel
@@ -15,9 +15,9 @@
1515
class Feature(GenericModel, Generic[Geom, Props]):
1616
"""Feature Model"""
1717

18-
type: str = Field(default="Feature", const=True)
19-
geometry: Optional[Geom] = None
20-
properties: Optional[Props] = None
18+
type: Literal["Feature"]
19+
geometry: Union[Geom, None] = Field(...)
20+
properties: Props = Field(...)
2121
id: Optional[str] = None
2222
bbox: Optional[BBox] = None
2323

@@ -29,9 +29,11 @@ class Config:
2929
@validator("geometry", pre=True, always=True)
3030
def set_geometry(cls, geometry: Any) -> Any:
3131
"""set geometry from geo interface or input"""
32-
if hasattr(geometry, "__geo_interface__"):
33-
return geometry.__geo_interface__
34-
return geometry
32+
if geometry is not None:
33+
if hasattr(geometry, "__geo_interface__"):
34+
return geometry.__geo_interface__
35+
36+
return geometry
3537

3638
@property
3739
def __geo_interface__(self) -> Dict[str, Any]:
@@ -44,23 +46,22 @@ def __geo_interface__(self) -> Dict[str, Any]:
4446
"geometry": self.geometry.__geo_interface__
4547
if self.geometry is not None
4648
else None,
49+
"properties": self.properties,
4750
}
51+
4852
if self.bbox:
4953
geo["bbox"] = self.bbox
5054

5155
if self.id:
5256
geo["id"] = self.id
5357

54-
if self.properties:
55-
geo["properties"] = self.properties
56-
5758
return geo
5859

5960

6061
class FeatureCollection(GenericModel, Generic[Geom, Props]):
6162
"""FeatureCollection Model"""
6263

63-
type: str = Field(default="FeatureCollection", const=True)
64+
type: Literal["FeatureCollection"]
6465
features: List[Feature[Geom, Props]]
6566
bbox: Optional[BBox] = None
6667

geojson_pydantic/geometries.py

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
"""pydantic models for GeoJSON Geometry objects."""
22

33
import abc
4-
from typing import Any, Dict, Iterator, List, Union
4+
from typing import Any, Dict, Iterator, List, Literal, Union
55

6-
from pydantic import BaseModel, Field, ValidationError, validator
6+
from pydantic import BaseModel, ValidationError, validator
77
from pydantic.error_wrappers import ErrorWrapper
88

99
from geojson_pydantic.types import (
@@ -56,7 +56,7 @@ def wkt(self) -> str:
5656
class Point(_GeometryBase):
5757
"""Point Model"""
5858

59-
type: str = Field(default="Point", const=True)
59+
type: Literal["Point"]
6060
coordinates: Position
6161

6262
@property
@@ -71,7 +71,7 @@ def _wkt_inset(self) -> str:
7171
class MultiPoint(_GeometryBase):
7272
"""MultiPoint Model"""
7373

74-
type: str = Field(default="MultiPoint", const=True)
74+
type: Literal["MultiPoint"]
7575
coordinates: MultiPointCoords
7676

7777
@property
@@ -80,14 +80,14 @@ def _wkt_inset(self) -> str:
8080

8181
@property
8282
def _wkt_coordinates(self) -> str:
83-
points = [Point(coordinates=p) for p in self.coordinates]
83+
points = [Point(type="Point", coordinates=p) for p in self.coordinates]
8484
return ", ".join(point._wkt_coordinates for point in points)
8585

8686

8787
class LineString(_GeometryBase):
8888
"""LineString Model"""
8989

90-
type: str = Field(default="LineString", const=True)
90+
type: Literal["LineString"]
9191
coordinates: LineStringCoords
9292

9393
@property
@@ -96,14 +96,14 @@ def _wkt_inset(self) -> str:
9696

9797
@property
9898
def _wkt_coordinates(self) -> str:
99-
points = [Point(coordinates=p) for p in self.coordinates]
99+
points = [Point(type="Point", coordinates=p) for p in self.coordinates]
100100
return ", ".join(point._wkt_coordinates for point in points)
101101

102102

103103
class MultiLineString(_GeometryBase):
104104
"""MultiLineString Model"""
105105

106-
type: str = Field(default="MultiLineString", const=True)
106+
type: Literal["MultiLineString"]
107107
coordinates: MultiLineStringCoords
108108

109109
@property
@@ -112,7 +112,9 @@ def _wkt_inset(self) -> str:
112112

113113
@property
114114
def _wkt_coordinates(self) -> str:
115-
lines = [LineString(coordinates=line) for line in self.coordinates]
115+
lines = [
116+
LineString(type="LineString", coordinates=line) for line in self.coordinates
117+
]
116118
return ",".join(f"({line._wkt_coordinates})" for line in lines)
117119

118120

@@ -131,7 +133,7 @@ def check_closure(cls, coordinates: List) -> List:
131133
class Polygon(_GeometryBase):
132134
"""Polygon Model"""
133135

134-
type: str = Field(default="Polygon", const=True)
136+
type: Literal["Polygon"]
135137
coordinates: PolygonCoords
136138

137139
@validator("coordinates")
@@ -161,27 +163,28 @@ def _wkt_inset(self) -> str:
161163
@property
162164
def _wkt_coordinates(self) -> str:
163165
ic = "".join(
164-
f", ({LinearRingGeom(coordinates=interior)._wkt_coordinates})"
166+
f", ({LinearRingGeom(type='LineString', coordinates=interior)._wkt_coordinates})"
165167
for interior in self.interiors
166168
)
167-
return f"({LinearRingGeom(coordinates=self.exterior)._wkt_coordinates}){ic}"
169+
return f"({LinearRingGeom(type='LineString', coordinates=self.exterior)._wkt_coordinates}){ic}"
168170

169171
@classmethod
170172
def from_bounds(
171173
cls, xmin: float, ymin: float, xmax: float, ymax: float
172174
) -> "Polygon":
173175
"""Create a Polygon geometry from a boundingbox."""
174176
return cls(
177+
type="Polygon",
175178
coordinates=[
176179
[(xmin, ymin), (xmax, ymin), (xmax, ymax), (xmin, ymax), (xmin, ymin)]
177-
]
180+
],
178181
)
179182

180183

181184
class MultiPolygon(_GeometryBase):
182185
"""MultiPolygon Model"""
183186

184-
type: str = Field(default="MultiPolygon", const=True)
187+
type: Literal["MultiPolygon"]
185188
coordinates: MultiPolygonCoords
186189

187190
@property
@@ -190,7 +193,9 @@ def _wkt_inset(self) -> str:
190193

191194
@property
192195
def _wkt_coordinates(self) -> str:
193-
polygons = [Polygon(coordinates=poly) for poly in self.coordinates]
196+
polygons = [
197+
Polygon(type="Polygon", coordinates=poly) for poly in self.coordinates
198+
]
194199
return ",".join(f"({poly._wkt_coordinates})" for poly in polygons)
195200

196201

@@ -200,7 +205,7 @@ def _wkt_coordinates(self) -> str:
200205
class GeometryCollection(BaseModel):
201206
"""GeometryCollection Model"""
202207

203-
type: str = Field(default="GeometryCollection", const=True)
208+
type: Literal["GeometryCollection"]
204209
geometries: List[Geometry]
205210

206211
def __iter__(self) -> Iterator[Geometry]: # type: ignore [override]

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name = "geojson-pydantic"
33
description = "Pydantic data models for the GeoJSON spec."
44
readme = "README.md"
5-
requires-python = ">=3.7"
5+
requires-python = ">=3.8"
66
license = {file = "LICENSE"}
77
authors = [
88
{name = "Drew Bollinger", email = "[email protected]"},
@@ -12,7 +12,6 @@ classifiers = [
1212
"Intended Audience :: Information Technology",
1313
"Intended Audience :: Science/Research",
1414
"License :: OSI Approved :: MIT License",
15-
"Programming Language :: Python :: 3.7",
1615
"Programming Language :: Python :: 3.8",
1716
"Programming Language :: Python :: 3.9",
1817
"Programming Language :: Python :: 3.10",

tests/test_features.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,18 @@ class GenericProperties(BaseModel):
6666

6767
def test_feature_collection_iteration():
6868
"""test if feature collection is iterable"""
69-
gc = FeatureCollection(features=[test_feature, test_feature])
69+
gc = FeatureCollection(
70+
type="FeatureCollection", features=[test_feature, test_feature]
71+
)
7072
assert hasattr(gc, "__geo_interface__")
7173
iter(gc)
7274

7375

7476
def test_geometry_collection_iteration():
7577
"""test if feature collection is iterable"""
76-
gc = FeatureCollection(features=[test_feature_geometry_collection])
78+
gc = FeatureCollection(
79+
type="FeatureCollection", features=[test_feature_geometry_collection]
80+
)
7781
assert hasattr(gc, "__geo_interface__")
7882
iter(gc)
7983

@@ -152,7 +156,7 @@ def test_generic_properties_should_raise_for_string():
152156

153157
def test_feature_collection_generic():
154158
fc = FeatureCollection[Polygon, GenericProperties](
155-
features=[test_feature, test_feature]
159+
type="FeatureCollection", features=[test_feature, test_feature]
156160
)
157161
assert len(fc) == 2
158162
assert type(fc[0].properties) == GenericProperties
@@ -163,7 +167,7 @@ def test_geo_interface_protocol():
163167
class Pointy:
164168
__geo_interface__ = {"type": "Point", "coordinates": (0.0, 0.0)}
165169

166-
feat = Feature(geometry=Pointy())
170+
feat = Feature(type="Feature", geometry=Pointy(), properties={})
167171
assert feat.geometry.dict() == Pointy.__geo_interface__
168172

169173

@@ -178,7 +182,9 @@ def test_feature_geo_interface_with_null_geometry():
178182

179183

180184
def test_feature_collection_geo_interface_with_null_geometry():
181-
fc = FeatureCollection(features=[test_feature_geom_null, test_feature])
185+
fc = FeatureCollection(
186+
type="FeatureCollection", features=[test_feature_geom_null, test_feature]
187+
)
182188
assert "bbox" not in fc.__geo_interface__
183189
assert "bbox" not in fc.__geo_interface__["features"][0]
184190
assert "bbox" in fc.__geo_interface__["features"][1]

0 commit comments

Comments
 (0)