Skip to content

Commit a4147ff

Browse files
Merge pull request #173 from developmentseed/feature/remove-custom-iter-len-getitem
remove conflicting iter len getitem methods
2 parents 11ff35b + fdebb3d commit a4147ff

File tree

5 files changed

+157
-33
lines changed

5 files changed

+157
-33
lines changed

CHANGELOG.md

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,103 @@ Note: Minor version `0.X.0` update might break the API, It's recommended to pin
88

99
## [unreleased]
1010

11+
## [2.0.0] - TBD
12+
13+
* remove custom `__iter__`, `__getitem__` and `__len__` methods from `GeometryCollection` class **breaking change**
14+
15+
```python
16+
from geojson_pydantic.geometries import GeometryCollection, Point, MultiPoint
17+
18+
geoms = GeometryCollection(
19+
type="GeometryCollection",
20+
geometries=[
21+
Point(type="Point", coordinates=(102.0, 0.5)),
22+
MultiPoint(type="MultiPoint", coordinates=[(100.0, 0.0), (101.0, 1.0)]),
23+
],
24+
)
25+
26+
########
27+
# Before
28+
for geom in geom: # __iter__
29+
pass
30+
31+
assert len(geoms) == 2 # __len__
32+
33+
_ = geoms[0] # __getitem__
34+
35+
#####
36+
# Now
37+
for geom in geom.iter(): # __iter__
38+
pass
39+
40+
assert geoms.length == 2 # __len__
41+
42+
_ = geoms.geometries[0] # __getitem__
43+
```
44+
45+
* remove custom `__iter__`, `__getitem__` and `__len__` methods from `FeatureCollection` class **breaking change**
46+
47+
```python
48+
from geojson_pydantic import FeatureCollection, Feature, Point
49+
50+
fc = FeatureCollection(
51+
type="FeatureCollection", features=[
52+
Feature(type="Feature", geometry=Point(type="Point", coordinates=(102.0, 0.5)), properties={"name": "point1"}),
53+
Feature(type="Feature", geometry=Point(type="Point", coordinates=(102.0, 1.5)), properties={"name": "point2"}),
54+
]
55+
)
56+
57+
########
58+
# Before
59+
for feat in fc: # __iter__
60+
pass
61+
62+
assert len(fc) == 2 # __len__
63+
64+
_ = fc[0] # __getitem__
65+
66+
#####
67+
# Now
68+
for feat in fc.iter(): # __iter__
69+
pass
70+
71+
assert fc.length == 2 # __len__
72+
73+
_ = fe.features[0] # __getitem__
74+
```
75+
76+
* make sure `GeometryCollection` are homogeneous for Z coordinates
77+
78+
```python
79+
from geojson_pydantic.geometries import Point, LineString, GeometryCollection
80+
# Before
81+
GeometryCollection(
82+
type="GeometryCollection",
83+
geometries=[
84+
Point(type="Point", coordinates=[0, 0]), # 2D point
85+
LineString(
86+
type="LineString", coordinates=[(0.0, 0.0, 0.0), (1.0, 1.0, 1.0)] # 3D LineString
87+
),
88+
],
89+
)
90+
>>> 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)])])
91+
92+
# Now
93+
GeometryCollection(
94+
type="GeometryCollection",
95+
geometries=[
96+
Point(type="Point", coordinates=[0, 0]), # 2D point
97+
LineString(
98+
type="LineString", coordinates=[(0.0, 0.0, 0.0), (1.0, 1.0, 1.0)] # 3D LineString
99+
),
100+
],
101+
)
102+
>>> ValidationError: 1 validation error for GeometryCollection
103+
geometries
104+
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]
105+
For further information visit https://errors.pydantic.dev/2.11/v/value_error
106+
```
107+
11108
## [1.2.0] - 2024-12-19
12109

13110
* 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
376473
### Added
377474
- Initial Release
378475

379-
[unreleased]: https://github.com/developmentseed/geojson-pydantic/compare/1.2.0...HEAD
476+
[unreleased]: https://github.com/developmentseed/geojson-pydantic/compare/2.0.0...HEAD
477+
[2.0.0]: https://github.com/developmentseed/geojson-pydantic/compare/1.2.0...2.0.0
380478
[1.2.0]: https://github.com/developmentseed/geojson-pydantic/compare/1.1.2...1.2.0
381479
[1.1.2]: https://github.com/developmentseed/geojson-pydantic/compare/1.1.1...1.1.2
382480
[1.1.1]: https://github.com/developmentseed/geojson-pydantic/compare/1.1.0...1.1.1

geojson_pydantic/features.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,11 @@ class FeatureCollection(_GeoJsonBase, Generic[Feat]):
3939
type: Literal["FeatureCollection"]
4040
features: List[Feat]
4141

42-
def __iter__(self) -> Iterator[Feat]: # type: ignore [override]
42+
def iter(self) -> Iterator[Feat]:
4343
"""iterate over features"""
4444
return iter(self.features)
4545

46-
def __len__(self) -> int:
46+
@property
47+
def length(self) -> int:
4748
"""return features length"""
4849
return len(self.features)
49-
50-
def __getitem__(self, index: int) -> Feat:
51-
"""get feature at a given index"""
52-
return self.features[index]

geojson_pydantic/geometries.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -251,18 +251,15 @@ class GeometryCollection(_GeoJsonBase):
251251
type: Literal["GeometryCollection"]
252252
geometries: List[Geometry]
253253

254-
def __iter__(self) -> Iterator[Geometry]: # type: ignore [override]
254+
def iter(self) -> Iterator[Geometry]:
255255
"""iterate over geometries"""
256256
return iter(self.geometries)
257257

258-
def __len__(self) -> int:
258+
@property
259+
def length(self) -> int:
259260
"""return geometries length"""
260261
return len(self.geometries)
261262

262-
def __getitem__(self, index: int) -> Geometry:
263-
"""get geometry at a given index"""
264-
return self.geometries[index]
265-
266263
@property
267264
def wkt(self) -> str:
268265
"""Return the Well Known Text representation."""
@@ -281,6 +278,11 @@ def wkt(self) -> str:
281278
z = " Z " if "Z" in geometries else " "
282279
return f"{self.type.upper()}{z}{geometries}"
283280

281+
@property
282+
def has_z(self) -> bool:
283+
"""Checks if any coordinates have a Z value."""
284+
return any(geom.has_z for geom in self.geometries)
285+
284286
@field_validator("geometries")
285287
def check_geometries(cls, geometries: List) -> List:
286288
"""Add warnings for conditions the spec does not explicitly forbid."""
@@ -302,6 +304,9 @@ def check_geometries(cls, geometries: List) -> List:
302304
stacklevel=1,
303305
)
304306

307+
if len({geom.has_z for geom in geometries}) == 2:
308+
raise ValueError("GeometryCollection cannot have mixed Z dimensionality.")
309+
305310
return geometries
306311

307312

tests/test_features.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,9 @@ def test_feature_collection_iteration():
9090
type="FeatureCollection", features=[test_feature, test_feature]
9191
)
9292
assert hasattr(gc, "__geo_interface__")
93-
iter(gc)
93+
assert list(iter(gc))
94+
assert len(list(gc.iter())) == 2
95+
assert dict(gc)
9496

9597

9698
def test_geometry_collection_iteration():
@@ -99,7 +101,9 @@ def test_geometry_collection_iteration():
99101
type="FeatureCollection", features=[test_feature_geometry_collection]
100102
)
101103
assert hasattr(gc, "__geo_interface__")
102-
iter(gc)
104+
assert list(iter(gc))
105+
assert len(list(gc.iter())) == 1
106+
assert dict(gc)
103107

104108

105109
def test_generic_properties_is_dict():
@@ -177,9 +181,11 @@ def test_feature_collection_generic():
177181
fc = FeatureCollection[Feature[Polygon, GenericProperties]](
178182
type="FeatureCollection", features=[test_feature, test_feature]
179183
)
180-
assert len(fc) == 2
181-
assert type(fc[0].properties) == GenericProperties
182-
assert type(fc[0].geometry) == Polygon
184+
assert fc.length == 2
185+
assert len(list(fc.iter())) == 2
186+
assert type(fc.features[0].properties) == GenericProperties
187+
assert type(fc.features[0].geometry) == Polygon
188+
assert dict(fc)
183189

184190

185191
def test_geo_interface_protocol():

tests/test_geometries.py

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,8 @@ def test_geometry_collection_iteration(coordinates):
498498
type="GeometryCollection", geometries=[polygon, multipolygon]
499499
)
500500
assert hasattr(gc, "__geo_interface__")
501-
iter(gc)
501+
assert len(list(gc.iter())) == 2
502+
assert list(iter(gc))
502503

503504

504505
@pytest.mark.parametrize(
@@ -511,7 +512,10 @@ def test_len_geometry_collection(coordinates):
511512
gc = GeometryCollection(
512513
type="GeometryCollection", geometries=[polygon, multipolygon]
513514
)
514-
assert len(gc) == 2
515+
assert gc.length == 2
516+
assert len(list(gc.iter())) == 2
517+
assert dict(gc)
518+
assert list(iter(gc))
515519

516520

517521
@pytest.mark.parametrize(
@@ -524,18 +528,20 @@ def test_getitem_geometry_collection(coordinates):
524528
gc = GeometryCollection(
525529
type="GeometryCollection", geometries=[polygon, multipolygon]
526530
)
527-
assert polygon == gc[0]
528-
assert multipolygon == gc[1]
531+
assert polygon == gc.geometries[0]
532+
assert multipolygon == gc.geometries[1]
529533

530534

531535
def test_wkt_mixed_geometry_collection():
532536
point = Point(type="Point", coordinates=(0.0, 0.0, 0.0))
533-
line_string = LineString(type="LineString", coordinates=[(0.0, 0.0), (1.0, 1.0)])
537+
line_string = LineString(
538+
type="LineString", coordinates=[(0.0, 0.0, 0.0), (1.0, 1.0, 1.0)]
539+
)
534540
assert (
535541
GeometryCollection(
536542
type="GeometryCollection", geometries=[point, line_string]
537543
).wkt
538-
== "GEOMETRYCOLLECTION Z (POINT Z (0.0 0.0 0.0), LINESTRING (0.0 0.0, 1.0 1.0))"
544+
== "GEOMETRYCOLLECTION Z (POINT Z (0.0 0.0 0.0), LINESTRING Z (0.0 0.0 0.0, 1.0 1.0 1.0))"
539545
)
540546

541547

@@ -548,6 +554,9 @@ def test_wkt_empty_geometry_collection():
548554

549555
def test_geometry_collection_warnings():
550556
point = Point(type="Point", coordinates=(0.0, 0.0, 0.0))
557+
line_string_z = LineString(
558+
type="LineString", coordinates=[(0.0, 0.0, 0.0), (1.0, 1.0, 1.0)]
559+
)
551560
line_string = LineString(type="LineString", coordinates=[(0.0, 0.0), (1.0, 1.0)])
552561

553562
# one geometry
@@ -571,18 +580,15 @@ def test_geometry_collection_warnings():
571580
type="GeometryCollection",
572581
geometries=[
573582
GeometryCollection(
574-
type="GeometryCollection", geometries=[point, line_string]
583+
type="GeometryCollection", geometries=[point, line_string_z]
575584
),
576585
point,
577586
],
578587
)
579588

580-
# homogeneous geometry
581-
with pytest.warns(
582-
UserWarning,
583-
match="GeometryCollection should not be used for homogeneous collections.",
584-
):
585-
GeometryCollection(type="GeometryCollection", geometries=[point, point])
589+
# homogeneous (Z) geometry
590+
with pytest.raises(ValidationError):
591+
GeometryCollection(type="GeometryCollection", geometries=[point, line_string])
586592

587593

588594
def test_polygon_from_bounds():
@@ -772,9 +778,9 @@ def test_wkt_empty_geometrycollection():
772778
"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)))",
773779
"MULTIPOLYGON EMPTY",
774780
"GEOMETRYCOLLECTION (POINT (0.0 0.0))",
781+
"GEOMETRYCOLLECTION (POLYGON EMPTY, MULTIPOLYGON (((0.0 0.0, 1.0 1.0, 2.0 2.0, 3.0 3.0, 0.0 0.0))))",
775782
"GEOMETRYCOLLECTION (POINT (0.0 0.0), MULTIPOINT ((0.0 0.0), (1.0 1.0)))",
776-
"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))))",
777-
"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)))",
783+
"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))))",
778784
"GEOMETRYCOLLECTION EMPTY",
779785
),
780786
)
@@ -839,10 +845,22 @@ def test_geometry_collection_serializer():
839845
LineString(type="LineString", coordinates=[(0.0, 0.0), (1.0, 1.0)]),
840846
],
841847
)
848+
assert not geom.has_z
842849
# bbox will be in the Dict
843850
assert "bbox" in geom.model_dump()
844851
assert "bbox" in geom.model_dump()["geometries"][0]
845852

853+
geom = GeometryCollection(
854+
type="GeometryCollection",
855+
geometries=[
856+
Point(type="Point", coordinates=[0, 0, 0]),
857+
LineString(
858+
type="LineString", coordinates=[(0.0, 0.0, 0.0), (1.0, 1.0, 1.0)]
859+
),
860+
],
861+
)
862+
assert geom.has_z
863+
846864
# bbox should not be in any Geometry nor at the top level
847865
geom_ser = json.loads(geom.model_dump_json())
848866
assert "bbox" not in geom_ser

0 commit comments

Comments
 (0)