Skip to content

Commit 1531edb

Browse files
committed
add from_attrs class method to construct object without type attribute
1 parent 32e37c8 commit 1531edb

File tree

4 files changed

+204
-4
lines changed

4 files changed

+204
-4
lines changed

geojson_pydantic/features.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Any, Dict, Generic, Iterator, List, Literal, Optional, TypeVar, Union
44

55
from pydantic import BaseModel, Field, StrictInt, StrictStr, field_validator
6+
from typing_extensions import Self
67

78
from geojson_pydantic.base import _GeoJsonBase
89
from geojson_pydantic.geometries import Geometry
@@ -29,6 +30,12 @@ def set_geometry(cls, geometry: Any) -> Any:
2930

3031
return geometry
3132

33+
@classmethod
34+
def from_attrs(cls, **kwargs: Any) -> Self:
35+
"""Create object from attributes."""
36+
t = kwargs.pop("type", "Feature")
37+
return cls(type=t, **kwargs)
38+
3239

3340
Feat = TypeVar("Feat", bound=Feature)
3441

@@ -47,3 +54,9 @@ def iter(self) -> Iterator[Feat]:
4754
def length(self) -> int:
4855
"""return features length"""
4956
return len(self.features)
57+
58+
@classmethod
59+
def from_attrs(cls, **kwargs: Any) -> Self:
60+
"""Create object from attributes."""
61+
t = kwargs.pop("type", "FeatureCollection")
62+
return cls(type=t, **kwargs)

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 from_attrs(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 from_attrs(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 from_attrs(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 from_attrs(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 from_attrs(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 from_attrs(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 from_attrs(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 from_attrs(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_features.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,3 +443,16 @@ def test_feature_collection_serializer():
443443
assert "bbox" not in featcoll_ser
444444
assert "bbox" not in featcoll_ser["features"][0]
445445
assert "bbox" not in featcoll_ser["features"][0]["geometry"]
446+
447+
448+
def test_class_method():
449+
"""test from_attrs method."""
450+
Feature.from_attrs(properties=None, geometry=None)
451+
Feature.from_attrs(type="Feature", properties=None, geometry=None)
452+
with pytest.raises(ValidationError):
453+
Feature.from_attrs(type="Feat", properties=None, geometry=None)
454+
455+
FeatureCollection.from_attrs(features=[test_feature])
456+
FeatureCollection.from_attrs(type="FeatureCollection", features=[test_feature])
457+
with pytest.raises(ValidationError):
458+
FeatureCollection.from_attrs(type="Feat", features=[test_feature])

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_from_attrs(obj, kwargs):
992+
"""Test Geometry object create with from_attrs."""
993+
assert obj.from_attrs(**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_from_attrs_invalid(obj, kwargs):
1036+
"""raise ValidationError with type is invalid."""
1037+
with pytest.raises(ValidationError):
1038+
obj.from_attrs(**kwargs)

0 commit comments

Comments
 (0)