Skip to content

Commit d65cdc0

Browse files
Merge pull request #94 from developmentseed/EnforceTypeGeometryProperties
Enforce type geometry properties
2 parents 1ff7b66 + cb6fbc4 commit d65cdc0

File tree

7 files changed

+121
-57
lines changed

7 files changed

+121
-57
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

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/)
66
and this project adheres to [Semantic Versioning](http://semver.org/).
77

8+
## [0.6.0] - TBD
9+
10+
- Remove python 3.7 support
11+
- Enforce required keys and avoid defaults. This aim to follow the geojson specification to the letter.
12+
13+
```python
14+
# Before
15+
Feature(geometry=Point(coordinates=(0,0)))
16+
17+
# Now
18+
Feature(
19+
type="Feature",
20+
geometry=Point(
21+
type="Point",
22+
coordinates=(0,0)
23+
),
24+
properties=None,
25+
)
26+
```
27+
28+
### Fixed
29+
30+
- Do not validates arbitrary dictionaries. Make `Type` a mandatory key for objects (https://github.com/developmentseed/geojson-pydantic/pull/94)
31+
832
## [0.5.0] - 2022-12-16
933

1034
### Added

geojson_pydantic/features.py

Lines changed: 8 additions & 8 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: Union[Props, None] = Field(...)
2121
id: Optional[str] = None
2222
bbox: Optional[BBox] = None
2323

@@ -31,6 +31,7 @@ def set_geometry(cls, geometry: Any) -> Any:
3131
"""set geometry from geo interface or input"""
3232
if hasattr(geometry, "__geo_interface__"):
3333
return geometry.__geo_interface__
34+
3435
return geometry
3536

3637
@property
@@ -44,23 +45,22 @@ def __geo_interface__(self) -> Dict[str, Any]:
4445
"geometry": self.geometry.__geo_interface__
4546
if self.geometry is not None
4647
else None,
48+
"properties": self.properties,
4749
}
50+
4851
if self.bbox:
4952
geo["bbox"] = self.bbox
5053

5154
if self.id:
5255
geo["id"] = self.id
5356

54-
if self.properties:
55-
geo["properties"] = self.properties
56-
5757
return geo
5858

5959

6060
class FeatureCollection(GenericModel, Generic[Geom, Props]):
6161
"""FeatureCollection Model"""
6262

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

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: 32 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,30 @@ 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]
191+
192+
193+
def test_feature_validation():
194+
"""Test default."""
195+
assert Feature(type="Feature", properties=None, geometry=None)
196+
197+
with pytest.raises(ValidationError):
198+
# should be type=Feature
199+
Feature(type="feature", properties=None, geometry=None)
200+
201+
with pytest.raises(ValidationError):
202+
# missing type
203+
Feature(properties=None, geometry=None)
204+
205+
with pytest.raises(ValidationError):
206+
# missing properties
207+
Feature(type="Feature", geometry=None)
208+
209+
with pytest.raises(ValidationError):
210+
# missing geometry
211+
Feature(type="Feature", properties=None)

0 commit comments

Comments
 (0)