Skip to content

Commit a148f8d

Browse files
committed
Use shared bbox validator.
1 parent af80dcb commit a148f8d

File tree

4 files changed

+58
-49
lines changed

4 files changed

+58
-49
lines changed

geojson_pydantic/features.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from geojson_pydantic.geo_interface import GeoInterfaceMixin
99
from geojson_pydantic.geometries import Geometry, GeometryCollection
10-
from geojson_pydantic.types import BBox
10+
from geojson_pydantic.types import BBox, validate_bbox
1111

1212
Props = TypeVar("Props", bound=Union[Dict[str, Any], BaseModel])
1313
Geom = TypeVar("Geom", bound=Union[Geometry, GeometryCollection])
@@ -22,6 +22,8 @@ class Feature(GenericModel, Generic[Geom, Props], GeoInterfaceMixin):
2222
id: Optional[Union[StrictInt, StrictStr]] = None
2323
bbox: Optional[BBox] = None
2424

25+
_validate_bbox = validator("bbox", allow_reuse=True)(validate_bbox)
26+
2527
@validator("geometry", pre=True, always=True)
2628
def set_geometry(cls, geometry: Any) -> Any:
2729
"""set geometry from geo interface or input"""
@@ -49,3 +51,5 @@ def __len__(self) -> int:
4951
def __getitem__(self, index: int) -> Feature:
5052
"""get feature at a given index"""
5153
return self.features[index]
54+
55+
_validate_bbox = validator("bbox", allow_reuse=True)(validate_bbox)

geojson_pydantic/geometries.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
MultiPolygonCoords,
2020
PolygonCoords,
2121
Position,
22+
validate_bbox,
2223
)
2324

2425

@@ -107,6 +108,8 @@ def wkt(self) -> str:
107108

108109
return wkt
109110

111+
_validate_bbox = validator("bbox", allow_reuse=True)(validate_bbox)
112+
110113

111114
class Point(_GeometryBase):
112115
"""Point Model"""
@@ -287,6 +290,8 @@ def wkt(self) -> str:
287290
z = " Z " if "Z" in geometries else " "
288291
return f"{self.type.upper()}{z}{geometries}"
289292

293+
_validate_bbox = validator("bbox", allow_reuse=True)(validate_bbox)
294+
290295
@validator("geometries")
291296
def check_geometries(cls, geometries: List) -> List:
292297
"""Add warnings for conditions the spec does not explicitly forbid."""

geojson_pydantic/types.py

Lines changed: 28 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,45 @@
11
"""Types for geojson_pydantic models"""
22

3-
from typing import TYPE_CHECKING, Any, Callable, Generator, List, Tuple, Union
3+
from typing import TYPE_CHECKING, List, Optional, Tuple, TypeVar, Union
44

5-
from pydantic import ConstrainedList, conlist
5+
from pydantic import conlist
66

7+
T = TypeVar("T")
78

8-
class _BBoxBase(ConstrainedList):
9-
"""Base Class with additional Validation for order."""
9+
BBox = Union[
10+
Tuple[float, float, float, float], Tuple[float, float, float, float, float, float]
11+
]
1012

11-
# This is needed because pydantic checks it rather than `item_type`
12-
__args__ = (float,)
1313

14-
item_type = float
14+
def validate_bbox(bbox: Optional[BBox]) -> Optional[BBox]:
15+
"""Validate BBox values are ordered correctly."""
16+
# If bbox is None, there is nothing to validate.
17+
if bbox is None:
18+
return None
1519

16-
@classmethod
17-
def __get_validators__(cls) -> Generator[Callable[..., Any], None, None]:
18-
"""Yield the validators."""
19-
yield from super().__get_validators__()
20-
yield cls.validate_bbox
20+
# A list to store any errors found so we can raise them all at once.
21+
errors: List[str] = []
2122

22-
@classmethod
23-
def validate_bbox(cls, bbox: List[float]) -> List[float]:
24-
"""Validate BBox values are ordered correctly."""
25-
if not bbox:
26-
return bbox
23+
# Determine where the second position starts. 2 for 2D, 3 for 3D.
24+
offset = len(bbox) // 2
2725

28-
offset = len(bbox) // 2
29-
errors: List[str] = []
30-
# Check X
31-
if bbox[0] > bbox[offset]:
32-
errors.append(f"Min X ({bbox[0]}) must be <= Max X ({bbox[offset]}).")
33-
# Check Y
34-
if bbox[1] > bbox[1 + offset]:
35-
errors.append(f"Min Y ({bbox[1]}) must be <= Max Y ({bbox[1 + offset]}).")
36-
# If 3D, check Z values.
37-
if offset > 2 and bbox[2] > bbox[2 + offset]:
38-
errors.append(f"Min Z ({bbox[2]}) must be <= Max Z ({bbox[2 + offset]}).")
26+
# Check X
27+
if bbox[0] > bbox[offset]:
28+
errors.append(f"Min X ({bbox[0]}) must be <= Max X ({bbox[offset]}).")
29+
# Check Y
30+
if bbox[1] > bbox[1 + offset]:
31+
errors.append(f"Min Y ({bbox[1]}) must be <= Max Y ({bbox[1 + offset]}).")
32+
# If 3D, check Z values.
33+
if offset > 2 and bbox[2] > bbox[2 + offset]:
34+
errors.append(f"Min Z ({bbox[2]}) must be <= Max Z ({bbox[2 + offset]}).")
3935

40-
if errors:
41-
raise ValueError("Invalid BBox. Error(s): " + " ".join(errors))
36+
# Raise any errors found.
37+
if errors:
38+
raise ValueError("Invalid BBox. Error(s): " + " ".join(errors))
4239

43-
return bbox
40+
return bbox
4441

4542

46-
class BBox2D(_BBoxBase):
47-
"""2D Bounding Box"""
48-
49-
min_items = 4
50-
max_items = 4
51-
52-
53-
class BBox3D(_BBoxBase):
54-
"""3D Bounding Box"""
55-
56-
min_items = 6
57-
max_items = 6
58-
59-
60-
BBox = Union[BBox3D, BBox2D]
6143
Position = Union[Tuple[float, float], Tuple[float, float, float]]
6244

6345
# Coordinate arrays

tests/test_features.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,8 +232,26 @@ def test_feature_validation():
232232

233233
with pytest.raises(ValidationError):
234234
# bad bbox2d
235-
Feature(type="Feature", properties=None, bbox=[100, 100, 0, 0])
235+
Feature(type="Feature", properties=None, bbox=(100, 100, 0, 0), geometry=None)
236236

237237
with pytest.raises(ValidationError):
238238
# bad bbox3d
239-
Feature(type="Feature", properties=None, bbox=[100, 100, 100, 0, 0, 0])
239+
Feature(
240+
type="Feature",
241+
properties=None,
242+
bbox=(100, 100, 100, 0, 0, 0),
243+
geometry=None,
244+
)
245+
246+
247+
def test_bbox_validation():
248+
# Some attempts at generic validation did not validate the types within
249+
# bbox before passing them to the function and resulted in TypeErrors.
250+
# This test exists to ensure that doesn't happen in the future.
251+
with pytest.raises(ValidationError):
252+
Feature(
253+
type="Feature",
254+
properties=None,
255+
bbox=(0, "a", 0, 1, 1, 1),
256+
geometry=None,
257+
)

0 commit comments

Comments
 (0)