diff --git a/geojson_pydantic/features.py b/geojson_pydantic/features.py index 089193e..7d7ae3f 100644 --- a/geojson_pydantic/features.py +++ b/geojson_pydantic/features.py @@ -3,6 +3,7 @@ from typing import Any, Dict, Generic, Iterator, List, Literal, Optional, TypeVar, Union from pydantic import BaseModel, Field, StrictInt, StrictStr, field_validator +from typing_extensions import Self from geojson_pydantic.base import _GeoJsonBase from geojson_pydantic.geometries import Geometry @@ -29,6 +30,12 @@ def set_geometry(cls, geometry: Any) -> Any: return geometry + @classmethod + def from_attrs(cls, **kwargs: Any) -> Self: + """Create object from attributes.""" + t = kwargs.pop("type", "Feature") + return cls(type=t, **kwargs) + Feat = TypeVar("Feat", bound=Feature) @@ -47,3 +54,9 @@ def iter(self) -> Iterator[Feat]: def length(self) -> int: """return features length""" return len(self.features) + + @classmethod + def from_attrs(cls, **kwargs: Any) -> Self: + """Create object from attributes.""" + t = kwargs.pop("type", "FeatureCollection") + return cls(type=t, **kwargs) diff --git a/geojson_pydantic/geometries.py b/geojson_pydantic/geometries.py index 3718612..3beead8 100644 --- a/geojson_pydantic/geometries.py +++ b/geojson_pydantic/geometries.py @@ -7,7 +7,7 @@ from typing import Any, Iterator, List, Literal, Union from pydantic import Field, field_validator -from typing_extensions import Annotated +from typing_extensions import Annotated, Self from geojson_pydantic.base import _GeoJsonBase from geojson_pydantic.types import ( @@ -105,6 +105,12 @@ def wkt(self) -> str: return wkt + @classmethod + @abc.abstractmethod + def from_attrs(cls, **kwargs: Any) -> Self: + """Create object from attributes.""" + ... + class Point(_GeometryBase): """Point Model""" @@ -121,6 +127,12 @@ def has_z(self) -> bool: """Checks if any coordinate has a Z value.""" return _position_has_z(self.coordinates) + @classmethod + def from_attrs(cls, **kwargs: Any) -> Self: + """Create object from attributes.""" + t = kwargs.pop("type", "Point") + return cls(type=t, **kwargs) + class MultiPoint(_GeometryBase): """MultiPoint Model""" @@ -140,6 +152,12 @@ def has_z(self) -> bool: """Checks if any coordinate has a Z value.""" return _position_list_has_z(self.coordinates) + @classmethod + def from_attrs(cls, **kwargs: Any) -> Self: + """Create object from attributes.""" + t = kwargs.pop("type", "MultiPoint") + return cls(type=t, **kwargs) + class LineString(_GeometryBase): """LineString Model""" @@ -156,6 +174,12 @@ def has_z(self) -> bool: """Checks if any coordinate has a Z value.""" return _position_list_has_z(self.coordinates) + @classmethod + def from_attrs(cls, **kwargs: Any) -> Self: + """Create object from attributes.""" + t = kwargs.pop("type", "LineString") + return cls(type=t, **kwargs) + class MultiLineString(_GeometryBase): """MultiLineString Model""" @@ -172,6 +196,12 @@ def has_z(self) -> bool: """Checks if any coordinate has a Z value.""" return _lines_has_z(self.coordinates) + @classmethod + def from_attrs(cls, **kwargs: Any) -> Self: + """Create object from attributes.""" + t = kwargs.pop("type", "MultiLineString") + return cls(type=t, **kwargs) + class Polygon(_GeometryBase): """Polygon Model""" @@ -209,9 +239,7 @@ def has_z(self) -> bool: return _lines_has_z(self.coordinates) @classmethod - def from_bounds( - cls, xmin: float, ymin: float, xmax: float, ymax: float - ) -> "Polygon": + def from_bounds(cls, xmin: float, ymin: float, xmax: float, ymax: float) -> Self: """Create a Polygon geometry from a boundingbox.""" return cls( type="Polygon", @@ -220,6 +248,12 @@ def from_bounds( ], ) + @classmethod + def from_attrs(cls, **kwargs: Any) -> Self: + """Create object from attributes.""" + t = kwargs.pop("type", "Polygon") + return cls(type=t, **kwargs) + class MultiPolygon(_GeometryBase): """MultiPolygon Model""" @@ -244,6 +278,12 @@ def check_closure(cls, coordinates: List) -> List: return coordinates + @classmethod + def from_attrs(cls, **kwargs: Any) -> Self: + """Create object from attributes.""" + t = kwargs.pop("type", "MultiPolygon") + return cls(type=t, **kwargs) + class GeometryCollection(_GeoJsonBase): """GeometryCollection Model""" @@ -309,6 +349,12 @@ def check_geometries(cls, geometries: List) -> List: return geometries + @classmethod + def from_attrs(cls, **kwargs: Any) -> Self: + """Create object from attributes.""" + t = kwargs.pop("type", "GeometryCollection") + return cls(type=t, **kwargs) + Geometry = Annotated[ Union[ diff --git a/tests/test_features.py b/tests/test_features.py index a4f7846..07e8828 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -443,3 +443,16 @@ def test_feature_collection_serializer(): assert "bbox" not in featcoll_ser assert "bbox" not in featcoll_ser["features"][0] assert "bbox" not in featcoll_ser["features"][0]["geometry"] + + +def test_class_method(): + """test from_attrs method.""" + Feature.from_attrs(properties=None, geometry=None) + Feature.from_attrs(type="Feature", properties=None, geometry=None) + with pytest.raises(ValidationError): + Feature.from_attrs(type="Feat", properties=None, geometry=None) + + FeatureCollection.from_attrs(features=[test_feature]) + FeatureCollection.from_attrs(type="FeatureCollection", features=[test_feature]) + with pytest.raises(ValidationError): + FeatureCollection.from_attrs(type="Feat", features=[test_feature]) diff --git a/tests/test_geometries.py b/tests/test_geometries.py index 21bc168..0a850ef 100644 --- a/tests/test_geometries.py +++ b/tests/test_geometries.py @@ -908,3 +908,131 @@ def test_geometry_collection_serializer(): assert "bbox" in geom_ser assert "bbox" not in geom_ser["geometries"][0] assert "bbox" not in geom_ser["geometries"][1] + + +@pytest.mark.parametrize( + "obj,kwargs", + ( + (Point, {"coordinates": [0, 0], "bbox": [0, 0, 0, 0]}), + (Point, {"coordinates": [0, 0]}), + (Point, {"type": "Point", "coordinates": [0, 0]}), + (MultiPoint, {"coordinates": [(0.0, 0.0)], "bbox": [0, 0, 0, 0]}), + (MultiPoint, {"coordinates": [(0.0, 0.0)]}), + (MultiPoint, {"type": "MultiPoint", "coordinates": [(0.0, 0.0)]}), + (LineString, {"coordinates": [(0.0, 0.0), (1.0, 1.0)], "bbox": [0, 0, 1, 1]}), + (LineString, {"coordinates": [(0.0, 0.0), (1.0, 1.0)]}), + (LineString, {"type": "LineString", "coordinates": [(0.0, 0.0), (1.0, 1.0)]}), + (MultiLineString, {"coordinates": [[(0.0, 0.0), (1.0, 1.0)]]}), + ( + MultiLineString, + {"coordinates": [[(0.0, 0.0), (1.0, 1.0)]], "bbox": [0, 0, 1, 1]}, + ), + ( + MultiLineString, + { + "type": "MultiLineString", + "coordinates": [[(0.0, 0.0), (1.0, 1.0)]], + }, + ), + ( + Polygon, + { + "coordinates": [[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]], + "bbox": [1.0, 2.0, 5.0, 6.0], + }, + ), + (Polygon, {"coordinates": [[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]]}), + ( + Polygon, + { + "type": "Polygon", + "coordinates": [[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]], + }, + ), + ( + MultiPolygon, + { + "coordinates": [[[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]]], + "bbox": [1.0, 2.0, 5.0, 6.0], + }, + ), + ( + MultiPolygon, + {"coordinates": [[[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]]]}, + ), + ( + MultiPolygon, + { + "type": "MultiPolygon", + "coordinates": [[[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]]], + }, + ), + ( + GeometryCollection, + { + "geometries": [ + {"type": "Point", "coordinates": [0, 0]}, + {"type": "MultiPoint", "coordinates": [[1, 1]]}, + ] + }, + ), + ( + GeometryCollection, + { + "type": "GeometryCollection", + "geometries": [ + {"type": "Point", "coordinates": [0, 0]}, + {"type": "MultiPoint", "coordinates": [[1, 1]]}, + ], + }, + ), + ), +) +def test_geometry_from_attrs(obj, kwargs): + """Test Geometry object create with from_attrs.""" + assert obj.from_attrs(**kwargs) + + +@pytest.mark.parametrize( + "obj,kwargs", + ( + (Point, {"type": "P", "coordinates": [0, 0]}), + (MultiPoint, {"type": "M", "coordinates": [(0.0, 0.0)]}), + (LineString, {"type": "L", "coordinates": [(0.0, 0.0), (1.0, 1.0)]}), + ( + MultiLineString, + { + "type": "M", + "coordinates": [[(0.0, 0.0), (1.0, 1.0)]], + }, + ), + ( + Polygon, + { + "type": "P", + "coordinates": [[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]], + }, + ), + ( + MultiPolygon, + { + "type": "M", + "coordinates": [[[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]]], + }, + ), + ( + GeometryCollection, + { + "type": "G", + "geometries": [ + {"type": "Point", "coordinates": [0, 0]}, + {"type": "MultiPoint", "coordinates": [[1, 1]]}, + ], + }, + ), + ), +) +def test_geometry_from_attrs_invalid(obj, kwargs): + """raise ValidationError with type is invalid.""" + with pytest.raises(ValidationError): + obj.from_attrs(**kwargs)