Skip to content

Commit 0218167

Browse files
Merge pull request #175 from developmentseed/feature/add-from-attrs-methods
add `from_attrs` class method to construct object without type attribute
2 parents 32e37c8 + fc6cde8 commit 0218167

File tree

4 files changed

+201
-5
lines changed

4 files changed

+201
-5
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@ Note: Minor version `0.X.0` update might break the API, It's recommended to pin
88

99
## [unreleased]
1010

11+
## [2.1.0] - 2025-10-08
12+
13+
* add `.create()` method to Geometry objects to create them without `type` key
14+
15+
```python
16+
from geojson_pydantic import Point
17+
18+
Point.create(coordinates=(0,0))
19+
>> Point(bbox=None, type='Point', coordinates=Position2D(longitude=0.0, latitude=0.0))
20+
```
21+
1122
## [2.0.0] - 2025-05-05
1223

1324
* remove custom `__iter__`, `__getitem__` and `__len__` methods from `GeometryCollection` class **breaking change**

docs/src/intro.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ assert fc.features[0].properties["name"] == "jeff"
3333
- `__geo_interface__`: GeoJSON-like protocol for geo-spatial (GIS) vector data ([spec](https://gist.github.com/sgillies/2217756#__geo_interface)).
3434
- `has_z`: returns true if any coordinate has a Z value.
3535
- `wkt`: returns the Well Known Text representation of the geometry.
36+
- `create`: create a geometry object without providing the `type` information
3637

3738
##### For Polygon geometry
3839

@@ -151,7 +152,7 @@ feat = MyPointFeatureModel(**geojson_feature)
151152
assert feat.properties.name == "drew"
152153
```
153154

154-
## Enforced Keys
155+
## Enforced `type` Keys
155156

156157
Starting with version `0.6.0`, geojson-pydantic's classes will not define default keys such has `type`, `geometry` or `properties`.
157158
This is to make sure the library does well its first goal, which is `validating` GeoJSON object based on the [specification](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.1)
@@ -187,3 +188,13 @@ Point(coordinates=(0,0))
187188
Point(type="Point", coordinates=(0,0))
188189
>> Point(type='Point', coordinates=(0.0, 0.0), bbox=None)
189190
```
191+
192+
Starting with `2.1.0`, users can use the `.create()` methods to create geometries without the `type` information
193+
194+
```python
195+
from geojson_pydantic import Point
196+
197+
Point.create(coordinates=(0,0))
198+
# is equivalent to
199+
Point(bbox=None, type='Point', coordinates=Position2D(longitude=0.0, latitude=0.0))
200+
```

geojson_pydantic/geometries.py

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from typing import Any, Iterator, List, Literal, Union
88

99
from pydantic import Field, field_validator
10-
from typing_extensions import Annotated
10+
from typing_extensions import Annotated, Self
1111

1212
from geojson_pydantic.base import _GeoJsonBase
1313
from geojson_pydantic.types import (
@@ -105,6 +105,12 @@ def wkt(self) -> str:
105105

106106
return wkt
107107

108+
@classmethod
109+
@abc.abstractmethod
110+
def create(cls, **kwargs: Any) -> Self:
111+
"""Create object from attributes."""
112+
...
113+
108114

109115
class Point(_GeometryBase):
110116
"""Point Model"""
@@ -121,6 +127,12 @@ def has_z(self) -> bool:
121127
"""Checks if any coordinate has a Z value."""
122128
return _position_has_z(self.coordinates)
123129

130+
@classmethod
131+
def create(cls, **kwargs: Any) -> Self:
132+
"""Create object from attributes."""
133+
t = kwargs.pop("type", "Point")
134+
return cls(type=t, **kwargs)
135+
124136

125137
class MultiPoint(_GeometryBase):
126138
"""MultiPoint Model"""
@@ -140,6 +152,12 @@ def has_z(self) -> bool:
140152
"""Checks if any coordinate has a Z value."""
141153
return _position_list_has_z(self.coordinates)
142154

155+
@classmethod
156+
def create(cls, **kwargs: Any) -> Self:
157+
"""Create object from attributes."""
158+
t = kwargs.pop("type", "MultiPoint")
159+
return cls(type=t, **kwargs)
160+
143161

144162
class LineString(_GeometryBase):
145163
"""LineString Model"""
@@ -156,6 +174,12 @@ def has_z(self) -> bool:
156174
"""Checks if any coordinate has a Z value."""
157175
return _position_list_has_z(self.coordinates)
158176

177+
@classmethod
178+
def create(cls, **kwargs: Any) -> Self:
179+
"""Create object from attributes."""
180+
t = kwargs.pop("type", "LineString")
181+
return cls(type=t, **kwargs)
182+
159183

160184
class MultiLineString(_GeometryBase):
161185
"""MultiLineString Model"""
@@ -172,6 +196,12 @@ def has_z(self) -> bool:
172196
"""Checks if any coordinate has a Z value."""
173197
return _lines_has_z(self.coordinates)
174198

199+
@classmethod
200+
def create(cls, **kwargs: Any) -> Self:
201+
"""Create object from attributes."""
202+
t = kwargs.pop("type", "MultiLineString")
203+
return cls(type=t, **kwargs)
204+
175205

176206
class Polygon(_GeometryBase):
177207
"""Polygon Model"""
@@ -209,9 +239,7 @@ def has_z(self) -> bool:
209239
return _lines_has_z(self.coordinates)
210240

211241
@classmethod
212-
def from_bounds(
213-
cls, xmin: float, ymin: float, xmax: float, ymax: float
214-
) -> "Polygon":
242+
def from_bounds(cls, xmin: float, ymin: float, xmax: float, ymax: float) -> Self:
215243
"""Create a Polygon geometry from a boundingbox."""
216244
return cls(
217245
type="Polygon",
@@ -220,6 +248,12 @@ def from_bounds(
220248
],
221249
)
222250

251+
@classmethod
252+
def create(cls, **kwargs: Any) -> Self:
253+
"""Create object from attributes."""
254+
t = kwargs.pop("type", "Polygon")
255+
return cls(type=t, **kwargs)
256+
223257

224258
class MultiPolygon(_GeometryBase):
225259
"""MultiPolygon Model"""
@@ -244,6 +278,12 @@ def check_closure(cls, coordinates: List) -> List:
244278

245279
return coordinates
246280

281+
@classmethod
282+
def create(cls, **kwargs: Any) -> Self:
283+
"""Create object from attributes."""
284+
t = kwargs.pop("type", "MultiPolygon")
285+
return cls(type=t, **kwargs)
286+
247287

248288
class GeometryCollection(_GeoJsonBase):
249289
"""GeometryCollection Model"""
@@ -309,6 +349,12 @@ def check_geometries(cls, geometries: List) -> List:
309349

310350
return geometries
311351

352+
@classmethod
353+
def create(cls, **kwargs: Any) -> Self:
354+
"""Create object from attributes."""
355+
t = kwargs.pop("type", "GeometryCollection")
356+
return cls(type=t, **kwargs)
357+
312358

313359
Geometry = Annotated[
314360
Union[

tests/test_geometries.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -908,3 +908,131 @@ def test_geometry_collection_serializer():
908908
assert "bbox" in geom_ser
909909
assert "bbox" not in geom_ser["geometries"][0]
910910
assert "bbox" not in geom_ser["geometries"][1]
911+
912+
913+
@pytest.mark.parametrize(
914+
"obj,kwargs",
915+
(
916+
(Point, {"coordinates": [0, 0], "bbox": [0, 0, 0, 0]}),
917+
(Point, {"coordinates": [0, 0]}),
918+
(Point, {"type": "Point", "coordinates": [0, 0]}),
919+
(MultiPoint, {"coordinates": [(0.0, 0.0)], "bbox": [0, 0, 0, 0]}),
920+
(MultiPoint, {"coordinates": [(0.0, 0.0)]}),
921+
(MultiPoint, {"type": "MultiPoint", "coordinates": [(0.0, 0.0)]}),
922+
(LineString, {"coordinates": [(0.0, 0.0), (1.0, 1.0)], "bbox": [0, 0, 1, 1]}),
923+
(LineString, {"coordinates": [(0.0, 0.0), (1.0, 1.0)]}),
924+
(LineString, {"type": "LineString", "coordinates": [(0.0, 0.0), (1.0, 1.0)]}),
925+
(MultiLineString, {"coordinates": [[(0.0, 0.0), (1.0, 1.0)]]}),
926+
(
927+
MultiLineString,
928+
{"coordinates": [[(0.0, 0.0), (1.0, 1.0)]], "bbox": [0, 0, 1, 1]},
929+
),
930+
(
931+
MultiLineString,
932+
{
933+
"type": "MultiLineString",
934+
"coordinates": [[(0.0, 0.0), (1.0, 1.0)]],
935+
},
936+
),
937+
(
938+
Polygon,
939+
{
940+
"coordinates": [[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]],
941+
"bbox": [1.0, 2.0, 5.0, 6.0],
942+
},
943+
),
944+
(Polygon, {"coordinates": [[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]]}),
945+
(
946+
Polygon,
947+
{
948+
"type": "Polygon",
949+
"coordinates": [[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]],
950+
},
951+
),
952+
(
953+
MultiPolygon,
954+
{
955+
"coordinates": [[[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]]],
956+
"bbox": [1.0, 2.0, 5.0, 6.0],
957+
},
958+
),
959+
(
960+
MultiPolygon,
961+
{"coordinates": [[[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]]]},
962+
),
963+
(
964+
MultiPolygon,
965+
{
966+
"type": "MultiPolygon",
967+
"coordinates": [[[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]]],
968+
},
969+
),
970+
(
971+
GeometryCollection,
972+
{
973+
"geometries": [
974+
{"type": "Point", "coordinates": [0, 0]},
975+
{"type": "MultiPoint", "coordinates": [[1, 1]]},
976+
]
977+
},
978+
),
979+
(
980+
GeometryCollection,
981+
{
982+
"type": "GeometryCollection",
983+
"geometries": [
984+
{"type": "Point", "coordinates": [0, 0]},
985+
{"type": "MultiPoint", "coordinates": [[1, 1]]},
986+
],
987+
},
988+
),
989+
),
990+
)
991+
def test_geometry_create(obj, kwargs):
992+
"""Test Geometry object create with new."""
993+
assert obj.create(**kwargs)
994+
995+
996+
@pytest.mark.parametrize(
997+
"obj,kwargs",
998+
(
999+
(Point, {"type": "P", "coordinates": [0, 0]}),
1000+
(MultiPoint, {"type": "M", "coordinates": [(0.0, 0.0)]}),
1001+
(LineString, {"type": "L", "coordinates": [(0.0, 0.0), (1.0, 1.0)]}),
1002+
(
1003+
MultiLineString,
1004+
{
1005+
"type": "M",
1006+
"coordinates": [[(0.0, 0.0), (1.0, 1.0)]],
1007+
},
1008+
),
1009+
(
1010+
Polygon,
1011+
{
1012+
"type": "P",
1013+
"coordinates": [[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]],
1014+
},
1015+
),
1016+
(
1017+
MultiPolygon,
1018+
{
1019+
"type": "M",
1020+
"coordinates": [[[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]]],
1021+
},
1022+
),
1023+
(
1024+
GeometryCollection,
1025+
{
1026+
"type": "G",
1027+
"geometries": [
1028+
{"type": "Point", "coordinates": [0, 0]},
1029+
{"type": "MultiPoint", "coordinates": [[1, 1]]},
1030+
],
1031+
},
1032+
),
1033+
),
1034+
)
1035+
def test_geometry_new_invalid(obj, kwargs):
1036+
"""raise ValidationError with type is invalid."""
1037+
with pytest.raises(ValidationError):
1038+
obj.create(**kwargs)

0 commit comments

Comments
 (0)