diff --git a/CHANGELOG.md b/CHANGELOG.md index bd9fee9..0454d3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,103 @@ Note: Minor version `0.X.0` update might break the API, It's recommended to pin ## [unreleased] +## [2.0.0] - TBD + +* remove custom `__iter__`, `__getitem__` and `__len__` methods from `GeometryCollection` class **breaking change** + + ```python + from geojson_pydantic.geometries import GeometryCollection, Point, MultiPoint + + geoms = GeometryCollection( + type="GeometryCollection", + geometries=[ + Point(type="Point", coordinates=(102.0, 0.5)), + MultiPoint(type="MultiPoint", coordinates=[(100.0, 0.0), (101.0, 1.0)]), + ], + ) + + ######## + # Before + for geom in geom: # __iter__ + pass + + assert len(geoms) == 2 # __len__ + + _ = geoms[0] # __getitem__ + + ##### + # Now + for geom in geom.iter(): # __iter__ + pass + + assert geoms.length == 2 # __len__ + + _ = geoms.geometries[0] # __getitem__ + ``` + +* remove custom `__iter__`, `__getitem__` and `__len__` methods from `FeatureCollection` class **breaking change** + + ```python + from geojson_pydantic import FeatureCollection, Feature, Point + + fc = FeatureCollection( + type="FeatureCollection", features=[ + Feature(type="Feature", geometry=Point(type="Point", coordinates=(102.0, 0.5)), properties={"name": "point1"}), + Feature(type="Feature", geometry=Point(type="Point", coordinates=(102.0, 1.5)), properties={"name": "point2"}), + ] + ) + + ######## + # Before + for feat in fc: # __iter__ + pass + + assert len(fc) == 2 # __len__ + + _ = fc[0] # __getitem__ + + ##### + # Now + for feat in fc.iter(): # __iter__ + pass + + assert fc.length == 2 # __len__ + + _ = fe.features[0] # __getitem__ + ``` + +* make sure `GeometryCollection` are homogeneous for Z coordinates + + ```python + from geojson_pydantic.geometries import Point, LineString, GeometryCollection + # Before + GeometryCollection( + type="GeometryCollection", + geometries=[ + Point(type="Point", coordinates=[0, 0]), # 2D point + LineString( + type="LineString", coordinates=[(0.0, 0.0, 0.0), (1.0, 1.0, 1.0)] # 3D LineString + ), + ], + ) + >>> GeometryCollection(bbox=None, type='GeometryCollection', geometries=[Point(bbox=None, type='Point', coordinates=Position3D(longitude=0.0, latitude=0.0, altitude=0.0)), LineString(bbox=None, type='LineString', coordinates=[Position3D(longitude=0.0, latitude=0.0, altitude=0.0), Position3D(longitude=1.0, latitude=1.0, altitude=1.0)])]) + + # Now + GeometryCollection( + type="GeometryCollection", + geometries=[ + Point(type="Point", coordinates=[0, 0]), # 2D point + LineString( + type="LineString", coordinates=[(0.0, 0.0, 0.0), (1.0, 1.0, 1.0)] # 3D LineString + ), + ], + ) + >>> ValidationError: 1 validation error for GeometryCollection + geometries + Value error, GeometryCollection cannot have mixed Z dimensionality. [type=value_error, input_value=[Point(bbox=None, type='P...de=1.0, altitude=1.0)])], input_type=list] + For further information visit https://errors.pydantic.dev/2.11/v/value_error + ``` + ## [1.2.0] - 2024-12-19 * drop python 3.8 support @@ -376,7 +473,8 @@ Although the type file was added in `0.2.0` it wasn't included in the distribute ### Added - Initial Release -[unreleased]: https://github.com/developmentseed/geojson-pydantic/compare/1.2.0...HEAD +[unreleased]: https://github.com/developmentseed/geojson-pydantic/compare/2.0.0...HEAD +[2.0.0]: https://github.com/developmentseed/geojson-pydantic/compare/1.2.0...2.0.0 [1.2.0]: https://github.com/developmentseed/geojson-pydantic/compare/1.1.2...1.2.0 [1.1.2]: https://github.com/developmentseed/geojson-pydantic/compare/1.1.1...1.1.2 [1.1.1]: https://github.com/developmentseed/geojson-pydantic/compare/1.1.0...1.1.1 diff --git a/geojson_pydantic/features.py b/geojson_pydantic/features.py index 738cdce..089193e 100644 --- a/geojson_pydantic/features.py +++ b/geojson_pydantic/features.py @@ -39,14 +39,11 @@ class FeatureCollection(_GeoJsonBase, Generic[Feat]): type: Literal["FeatureCollection"] features: List[Feat] - def __iter__(self) -> Iterator[Feat]: # type: ignore [override] + def iter(self) -> Iterator[Feat]: """iterate over features""" return iter(self.features) - def __len__(self) -> int: + @property + def length(self) -> int: """return features length""" return len(self.features) - - def __getitem__(self, index: int) -> Feat: - """get feature at a given index""" - return self.features[index] diff --git a/geojson_pydantic/geometries.py b/geojson_pydantic/geometries.py index b1f556a..3718612 100644 --- a/geojson_pydantic/geometries.py +++ b/geojson_pydantic/geometries.py @@ -251,18 +251,15 @@ class GeometryCollection(_GeoJsonBase): type: Literal["GeometryCollection"] geometries: List[Geometry] - def __iter__(self) -> Iterator[Geometry]: # type: ignore [override] + def iter(self) -> Iterator[Geometry]: """iterate over geometries""" return iter(self.geometries) - def __len__(self) -> int: + @property + def length(self) -> int: """return geometries length""" return len(self.geometries) - def __getitem__(self, index: int) -> Geometry: - """get geometry at a given index""" - return self.geometries[index] - @property def wkt(self) -> str: """Return the Well Known Text representation.""" @@ -281,6 +278,11 @@ def wkt(self) -> str: z = " Z " if "Z" in geometries else " " return f"{self.type.upper()}{z}{geometries}" + @property + def has_z(self) -> bool: + """Checks if any coordinates have a Z value.""" + return any(geom.has_z for geom in self.geometries) + @field_validator("geometries") def check_geometries(cls, geometries: List) -> List: """Add warnings for conditions the spec does not explicitly forbid.""" @@ -302,6 +304,9 @@ def check_geometries(cls, geometries: List) -> List: stacklevel=1, ) + if len({geom.has_z for geom in geometries}) == 2: + raise ValueError("GeometryCollection cannot have mixed Z dimensionality.") + return geometries diff --git a/tests/test_features.py b/tests/test_features.py index 9286bef..a4f7846 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -90,7 +90,9 @@ def test_feature_collection_iteration(): type="FeatureCollection", features=[test_feature, test_feature] ) assert hasattr(gc, "__geo_interface__") - iter(gc) + assert list(iter(gc)) + assert len(list(gc.iter())) == 2 + assert dict(gc) def test_geometry_collection_iteration(): @@ -99,7 +101,9 @@ def test_geometry_collection_iteration(): type="FeatureCollection", features=[test_feature_geometry_collection] ) assert hasattr(gc, "__geo_interface__") - iter(gc) + assert list(iter(gc)) + assert len(list(gc.iter())) == 1 + assert dict(gc) def test_generic_properties_is_dict(): @@ -177,9 +181,11 @@ def test_feature_collection_generic(): fc = FeatureCollection[Feature[Polygon, GenericProperties]]( type="FeatureCollection", features=[test_feature, test_feature] ) - assert len(fc) == 2 - assert type(fc[0].properties) == GenericProperties - assert type(fc[0].geometry) == Polygon + assert fc.length == 2 + assert len(list(fc.iter())) == 2 + assert type(fc.features[0].properties) == GenericProperties + assert type(fc.features[0].geometry) == Polygon + assert dict(fc) def test_geo_interface_protocol(): diff --git a/tests/test_geometries.py b/tests/test_geometries.py index aac4dd3..21bc168 100644 --- a/tests/test_geometries.py +++ b/tests/test_geometries.py @@ -498,7 +498,8 @@ def test_geometry_collection_iteration(coordinates): type="GeometryCollection", geometries=[polygon, multipolygon] ) assert hasattr(gc, "__geo_interface__") - iter(gc) + assert len(list(gc.iter())) == 2 + assert list(iter(gc)) @pytest.mark.parametrize( @@ -511,7 +512,10 @@ def test_len_geometry_collection(coordinates): gc = GeometryCollection( type="GeometryCollection", geometries=[polygon, multipolygon] ) - assert len(gc) == 2 + assert gc.length == 2 + assert len(list(gc.iter())) == 2 + assert dict(gc) + assert list(iter(gc)) @pytest.mark.parametrize( @@ -524,18 +528,20 @@ def test_getitem_geometry_collection(coordinates): gc = GeometryCollection( type="GeometryCollection", geometries=[polygon, multipolygon] ) - assert polygon == gc[0] - assert multipolygon == gc[1] + assert polygon == gc.geometries[0] + assert multipolygon == gc.geometries[1] def test_wkt_mixed_geometry_collection(): point = Point(type="Point", coordinates=(0.0, 0.0, 0.0)) - line_string = LineString(type="LineString", coordinates=[(0.0, 0.0), (1.0, 1.0)]) + line_string = LineString( + type="LineString", coordinates=[(0.0, 0.0, 0.0), (1.0, 1.0, 1.0)] + ) assert ( GeometryCollection( type="GeometryCollection", geometries=[point, line_string] ).wkt - == "GEOMETRYCOLLECTION Z (POINT Z (0.0 0.0 0.0), LINESTRING (0.0 0.0, 1.0 1.0))" + == "GEOMETRYCOLLECTION Z (POINT Z (0.0 0.0 0.0), LINESTRING Z (0.0 0.0 0.0, 1.0 1.0 1.0))" ) @@ -548,6 +554,9 @@ def test_wkt_empty_geometry_collection(): def test_geometry_collection_warnings(): point = Point(type="Point", coordinates=(0.0, 0.0, 0.0)) + line_string_z = LineString( + type="LineString", coordinates=[(0.0, 0.0, 0.0), (1.0, 1.0, 1.0)] + ) line_string = LineString(type="LineString", coordinates=[(0.0, 0.0), (1.0, 1.0)]) # one geometry @@ -571,18 +580,15 @@ def test_geometry_collection_warnings(): type="GeometryCollection", geometries=[ GeometryCollection( - type="GeometryCollection", geometries=[point, line_string] + type="GeometryCollection", geometries=[point, line_string_z] ), point, ], ) - # homogeneous geometry - with pytest.warns( - UserWarning, - match="GeometryCollection should not be used for homogeneous collections.", - ): - GeometryCollection(type="GeometryCollection", geometries=[point, point]) + # homogeneous (Z) geometry + with pytest.raises(ValidationError): + GeometryCollection(type="GeometryCollection", geometries=[point, line_string]) def test_polygon_from_bounds(): @@ -772,9 +778,9 @@ def test_wkt_empty_geometrycollection(): "MULTIPOLYGON Z (((0.0 0.0 0.0, 1.0 1.0 0.0, 2.0 2.0 0.0, 3.0 3.0 0.0, 0.0 0.0 0.0)), ((1.0 1.0 0.0, 2.0 2.0 0.0, 3.0 3.0 0.0, 4.0 4.0 0.0, 1.0 1.0 0.0)))", "MULTIPOLYGON EMPTY", "GEOMETRYCOLLECTION (POINT (0.0 0.0))", + "GEOMETRYCOLLECTION (POLYGON EMPTY, MULTIPOLYGON (((0.0 0.0, 1.0 1.0, 2.0 2.0, 3.0 3.0, 0.0 0.0))))", "GEOMETRYCOLLECTION (POINT (0.0 0.0), MULTIPOINT ((0.0 0.0), (1.0 1.0)))", - "GEOMETRYCOLLECTION Z (POLYGON EMPTY, MULTIPOLYGON Z (((0.0 0.0 0.0, 1.0 1.0 0.0, 2.0 2.0 0.0, 3.0 3.0 0.0, 0.0 0.0 0.0))))", - "GEOMETRYCOLLECTION Z (LINESTRING Z (0.0 0.0 0.0, 1.0 1.0 1.0, 2.0 2.0 2.0), MULTILINESTRING ((0.0 0.0, 1.0 1.0), (1.0 1.0, 2.0 2.0)))", + "GEOMETRYCOLLECTION Z (POLYGON Z ((0.0 0.0 0.0, 1.0 1.0 0.0, 2.0 2.0 0.0, 3.0 3.0 0.0, 0.0 0.0 0.0)), MULTIPOLYGON Z (((0.0 0.0 0.0, 1.0 1.0 0.0, 2.0 2.0 0.0, 3.0 3.0 0.0, 0.0 0.0 0.0))))", "GEOMETRYCOLLECTION EMPTY", ), ) @@ -839,10 +845,22 @@ def test_geometry_collection_serializer(): LineString(type="LineString", coordinates=[(0.0, 0.0), (1.0, 1.0)]), ], ) + assert not geom.has_z # bbox will be in the Dict assert "bbox" in geom.model_dump() assert "bbox" in geom.model_dump()["geometries"][0] + geom = GeometryCollection( + type="GeometryCollection", + geometries=[ + Point(type="Point", coordinates=[0, 0, 0]), + LineString( + type="LineString", coordinates=[(0.0, 0.0, 0.0), (1.0, 1.0, 1.0)] + ), + ], + ) + assert geom.has_z + # bbox should not be in any Geometry nor at the top level geom_ser = json.loads(geom.model_dump_json()) assert "bbox" not in geom_ser