diff --git a/CHANGELOG.md b/CHANGELOG.md index 9de8e6c..05c577a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,17 @@ Note: Minor version `0.X.0` update might break the API, It's recommended to pin ## [unreleased] +## [2.1.0] - 2025-10-08 + +* add `.create()` method to Geometry objects to create them without `type` key + + ```python + from geojson_pydantic import Point + + Point.create(coordinates=(0,0)) + >> Point(bbox=None, type='Point', coordinates=Position2D(longitude=0.0, latitude=0.0)) + ``` + ## [2.0.0] - 2025-05-05 * remove custom `__iter__`, `__getitem__` and `__len__` methods from `GeometryCollection` class **breaking change** diff --git a/docs/src/intro.md b/docs/src/intro.md index 3f262fa..0108213 100644 --- a/docs/src/intro.md +++ b/docs/src/intro.md @@ -33,6 +33,7 @@ assert fc.features[0].properties["name"] == "jeff" - `__geo_interface__`: GeoJSON-like protocol for geo-spatial (GIS) vector data ([spec](https://gist.github.com/sgillies/2217756#__geo_interface)). - `has_z`: returns true if any coordinate has a Z value. - `wkt`: returns the Well Known Text representation of the geometry. +- `create`: create a geometry object without providing the `type` information ##### For Polygon geometry @@ -151,7 +152,7 @@ feat = MyPointFeatureModel(**geojson_feature) assert feat.properties.name == "drew" ``` -## Enforced Keys +## Enforced `type` Keys Starting with version `0.6.0`, geojson-pydantic's classes will not define default keys such has `type`, `geometry` or `properties`. 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)) Point(type="Point", coordinates=(0,0)) >> Point(type='Point', coordinates=(0.0, 0.0), bbox=None) ``` + +Starting with `2.1.0`, users can use the `.create()` methods to create geometries without the `type` information + +```python +from geojson_pydantic import Point + +Point.create(coordinates=(0,0)) +# is equivalent to +Point(bbox=None, type='Point', coordinates=Position2D(longitude=0.0, latitude=0.0)) +``` \ No newline at end of file diff --git a/geojson_pydantic/geometries.py b/geojson_pydantic/geometries.py index 3718612..3d24c3e 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 create(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 create(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 create(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 create(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 create(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 create(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 create(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 create(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_geometries.py b/tests/test_geometries.py index 21bc168..e289d2b 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_create(obj, kwargs): + """Test Geometry object create with new.""" + assert obj.create(**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_new_invalid(obj, kwargs): + """raise ValidationError with type is invalid.""" + with pytest.raises(ValidationError): + obj.create(**kwargs)