Skip to content

remove conflicting iter len getitem methods #173

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 99 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
9 changes: 3 additions & 6 deletions geojson_pydantic/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
17 changes: 11 additions & 6 deletions geojson_pydantic/geometries.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -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."""
Expand All @@ -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


Expand Down
16 changes: 11 additions & 5 deletions tests/test_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -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():
Expand Down Expand Up @@ -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():
Expand Down
48 changes: 33 additions & 15 deletions tests/test_geometries.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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))"
)


Expand All @@ -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
Expand All @@ -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():
Expand Down Expand Up @@ -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))))",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out there was some change in GEOS and now we can't have a mixed of geometries with or without Z coordinates.

We will now raise a validation error as well

"GEOMETRYCOLLECTION EMPTY",
),
)
Expand Down Expand Up @@ -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
Expand Down