Skip to content

Commit 40dbafb

Browse files
committed
update for pydantic 2.0
1 parent 8a07ab8 commit 40dbafb

File tree

8 files changed

+98
-35
lines changed

8 files changed

+98
-35
lines changed

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ repos:
2323
args: ["--fix"]
2424

2525
- repo: https://github.com/pre-commit/mirrors-mypy
26-
rev: v0.991
26+
rev: v1.4.1
2727
hooks:
2828
- id: mypy
2929
language_version: python
3030
# No reason to run if only tests have changed. They intentionally break typing.
3131
exclude: tests/.*
3232
additional_dependencies:
33-
- pydantic~=1.0
33+
- pydantic~=2.0

CHANGELOG.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,31 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
66

77
Note: Minor version `0.X.0` update might break the API, It's recommanded to pin geojson-pydantic to minor version: `geojson-pydantic>=0.6,<0.7`
88

9+
## [unreleased]
10+
11+
### Added
12+
13+
* more tests for `GeometryCollection` warnings
14+
15+
### changed
16+
17+
* update pydantic requirement to `~=2.0`
18+
19+
* raise `ValueError` in `geomtries.parse_geometry_obj` instead of `ValidationError`
20+
21+
```python
22+
# before
23+
parse_geometry_obj({"type": "This type", "obviously": "doesn't exist"})
24+
>> ValidationError
25+
26+
# now
27+
parse_geometry_obj({"type": "This type", "obviously": "doesn't exist"})
28+
>> ValueError("Unknown type: This type")
29+
```
30+
931
## [0.6.3] - 2023-07-02
1032

11-
* limit pydantic requirement to `~=1.0``
33+
* limit pydantic requirement to `~=1.0`
1234

1335
## [0.6.2] - 2023-05-16
1436

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ from typing import Optional
146146
MyPointFeatureModel = Feature[Optional[Point], Dict]
147147

148148
assert MyPointFeatureModel(type="Feature", geometry=None, properties={}).geometry is None
149-
assert MyPointFeatureModel(type="Feature", geometry=Point(coordinates=(0,0)), properties={}).geometry is not None
149+
assert MyPointFeatureModel(type="Feature", geometry=Point(type="Point", coordinates=(0,0)), properties={}).geometry is not None
150150
```
151151

152152
And now with constrained properties
@@ -157,7 +157,7 @@ from pydantic import BaseModel, constr
157157

158158
# Define a Feature model with Geometry as `Point` and Properties as a constrained Model
159159
class MyProps(BaseModel):
160-
name: constr(regex=r'^(drew|vincent)$')
160+
name: constr(pattern=r'^(drew|vincent)$')
161161

162162
MyPointFeatureModel = Feature[Point, MyProps]
163163

geojson_pydantic/features.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22

33
from typing import Any, Dict, Generic, Iterator, List, Literal, Optional, TypeVar, Union
44

5-
from pydantic import BaseModel, Field, StrictInt, StrictStr, validator
6-
from pydantic.generics import GenericModel
5+
from pydantic import BaseModel, Field, StrictInt, StrictStr, field_validator
76

87
from geojson_pydantic.geo_interface import GeoInterfaceMixin
98
from geojson_pydantic.geometries import Geometry, GeometryCollection
@@ -13,7 +12,7 @@
1312
Geom = TypeVar("Geom", bound=Union[Geometry, GeometryCollection])
1413

1514

16-
class Feature(GenericModel, Generic[Geom, Props], GeoInterfaceMixin):
15+
class Feature(BaseModel, Generic[Geom, Props], GeoInterfaceMixin):
1716
"""Feature Model"""
1817

1918
type: Literal["Feature"]
@@ -22,9 +21,9 @@ class Feature(GenericModel, Generic[Geom, Props], GeoInterfaceMixin):
2221
id: Optional[Union[StrictInt, StrictStr]] = None
2322
bbox: Optional[BBox] = None
2423

25-
_validate_bbox = validator("bbox", allow_reuse=True)(validate_bbox)
24+
_validate_bbox = field_validator("bbox")(validate_bbox)
2625

27-
@validator("geometry", pre=True, always=True)
26+
@field_validator("geometry", mode="before")
2827
def set_geometry(cls, geometry: Any) -> Any:
2928
"""set geometry from geo interface or input"""
3029
if hasattr(geometry, "__geo_interface__"):
@@ -33,7 +32,7 @@ def set_geometry(cls, geometry: Any) -> Any:
3332
return geometry
3433

3534

36-
class FeatureCollection(GenericModel, Generic[Geom, Props], GeoInterfaceMixin):
35+
class FeatureCollection(BaseModel, Generic[Geom, Props], GeoInterfaceMixin):
3736
"""FeatureCollection Model"""
3837

3938
type: Literal["FeatureCollection"]
@@ -52,4 +51,4 @@ def __getitem__(self, index: int) -> Feature:
5251
"""get feature at a given index"""
5352
return self.features[index]
5453

55-
_validate_bbox = validator("bbox", allow_reuse=True)(validate_bbox)
54+
_validate_bbox = field_validator("bbox")(validate_bbox)

geojson_pydantic/geometries.py

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
import warnings
66
from typing import Any, Iterator, List, Literal, Optional, Union
77

8-
from pydantic import BaseModel, Field, ValidationError, validator
9-
from pydantic.error_wrappers import ErrorWrapper
8+
from pydantic import BaseModel, Field, field_validator
109
from typing_extensions import Annotated
1110

1211
from geojson_pydantic.geo_interface import GeoInterfaceMixin
@@ -108,7 +107,7 @@ def wkt(self) -> str:
108107

109108
return wkt
110109

111-
_validate_bbox = validator("bbox", allow_reuse=True)(validate_bbox)
110+
_validate_bbox = field_validator("bbox")(validate_bbox)
112111

113112

114113
class Point(_GeometryBase):
@@ -185,7 +184,7 @@ def __wkt_coordinates__(self, coordinates: Any, force_z: bool) -> str:
185184
"""return WKT coordinates."""
186185
return _lines_wtk_coordinates(coordinates, force_z)
187186

188-
@validator("coordinates")
187+
@field_validator("coordinates")
189188
def check_closure(cls, coordinates: List) -> List:
190189
"""Validate that Polygon is closed (first and last coordinate are the same)."""
191190
if any(ring[-1] != ring[0] for ring in coordinates):
@@ -238,7 +237,7 @@ def has_z(self) -> bool:
238237
"""Checks if any coordinates have a Z value."""
239238
return any(_lines_has_z(polygon) for polygon in self.coordinates)
240239

241-
@validator("coordinates")
240+
@field_validator("coordinates")
242241
def check_closure(cls, coordinates: List) -> List:
243242
"""Validate that Polygon is closed (first and last coordinate are the same)."""
244243
if any(ring[-1] != ring[0] for polygon in coordinates for ring in polygon):
@@ -290,23 +289,26 @@ def wkt(self) -> str:
290289
z = " Z " if "Z" in geometries else " "
291290
return f"{self.type.upper()}{z}{geometries}"
292291

293-
_validate_bbox = validator("bbox", allow_reuse=True)(validate_bbox)
292+
_validate_bbox = field_validator("bbox")(validate_bbox)
294293

295-
@validator("geometries")
294+
@field_validator("geometries")
296295
def check_geometries(cls, geometries: List) -> List:
297296
"""Add warnings for conditions the spec does not explicitly forbid."""
298297
if len(geometries) == 1:
299298
warnings.warn(
300299
"GeometryCollection should not be used for single geometries."
301300
)
301+
302302
if any(geom.type == "GeometryCollection" for geom in geometries):
303303
warnings.warn(
304304
"GeometryCollection should not be used for nested GeometryCollections."
305305
)
306+
306307
if len({geom.type for geom in geometries}) == 1:
307308
warnings.warn(
308309
"GeometryCollection should not be used for homogeneous collections."
309310
)
311+
310312
return geometries
311313

312314

@@ -316,25 +318,24 @@ def parse_geometry_obj(obj: Any) -> Geometry:
316318
reads the `"type"` field and returns the correct pydantic Geometry model.
317319
"""
318320
if "type" not in obj:
319-
raise ValidationError(
320-
errors=[
321-
ErrorWrapper(ValueError("Missing 'type' field in geometry"), loc="type")
322-
],
323-
model=_GeometryBase,
324-
)
321+
raise ValueError("Missing 'type' field in geometry")
322+
325323
if obj["type"] == "Point":
326324
return Point.parse_obj(obj)
325+
327326
elif obj["type"] == "MultiPoint":
328327
return MultiPoint.parse_obj(obj)
328+
329329
elif obj["type"] == "LineString":
330330
return LineString.parse_obj(obj)
331+
331332
elif obj["type"] == "MultiLineString":
332333
return MultiLineString.parse_obj(obj)
334+
333335
elif obj["type"] == "Polygon":
334336
return Polygon.parse_obj(obj)
337+
335338
elif obj["type"] == "MultiPolygon":
336339
return MultiPolygon.parse_obj(obj)
337-
raise ValidationError(
338-
errors=[ErrorWrapper(ValueError("Unknown type"), loc="type")],
339-
model=_GeometryBase,
340-
)
340+
341+
raise ValueError(f"Unknown type: {obj['type']}")

geojson_pydantic/types.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ def validate_bbox(bbox: Optional[BBox]) -> Optional[BBox]:
4848
LineStringCoords = List[Position]
4949
LinearRing = List[Position]
5050
else:
51-
LineStringCoords = conlist(Position, min_items=2)
52-
LinearRing = conlist(Position, min_items=4)
51+
LineStringCoords = conlist(Position, min_length=2)
52+
LinearRing = conlist(Position, min_length=4)
5353

5454
MultiPointCoords = List[Position]
5555
MultiLineStringCoords = List[LineStringCoords]

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ classifiers = [
2020
"Typing :: Typed",
2121
]
2222
dynamic = ["version"]
23-
dependencies = ["pydantic~=1.0"]
23+
dependencies = ["pydantic~=2.0"]
2424

2525
[project.optional-dependencies]
2626
test = ["pytest", "pytest-cov", "shapely"]

tests/test_geometries.py

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -428,11 +428,13 @@ def test_parse_geometry_obj_multi_polygon():
428428

429429

430430
def test_parse_geometry_obj_invalid_type():
431-
with pytest.raises(ValidationError):
431+
with pytest.raises(ValueError):
432432
parse_geometry_obj({"type": "This type", "obviously": "doesn't exist"})
433-
with pytest.raises(ValidationError):
433+
434+
with pytest.raises(ValueError):
434435
parse_geometry_obj({"type": "", "obviously": "doesn't exist"})
435-
with pytest.raises(ValidationError):
436+
437+
with pytest.raises(ValueError):
436438
parse_geometry_obj({})
437439

438440

@@ -502,6 +504,45 @@ def test_wkt_empty_geometry_collection():
502504
assert_wkt_equivalence(gc)
503505

504506

507+
def test_geometry_collection_warnings():
508+
point = Point(type="Point", coordinates=(0.0, 0.0, 0.0))
509+
line_string = LineString(type="LineString", coordinates=[(0.0, 0.0), (1.0, 1.0)])
510+
511+
# one geometry
512+
with pytest.warns(
513+
UserWarning,
514+
match="GeometryCollection should not be used for single geometries.",
515+
):
516+
GeometryCollection(
517+
type="GeometryCollection",
518+
geometries=[
519+
point,
520+
],
521+
)
522+
523+
# collections of collections
524+
with pytest.warns(
525+
UserWarning,
526+
match="GeometryCollection should not be used for nested GeometryCollections.",
527+
):
528+
GeometryCollection(
529+
type="GeometryCollection",
530+
geometries=[
531+
GeometryCollection(
532+
type="GeometryCollection", geometries=[point, line_string]
533+
),
534+
point,
535+
],
536+
)
537+
538+
# homogeneous geometry
539+
with pytest.warns(
540+
UserWarning,
541+
match="GeometryCollection should not be used for homogeneous collections.",
542+
):
543+
GeometryCollection(type="GeometryCollection", geometries=[point, point])
544+
545+
505546
def test_polygon_from_bounds():
506547
"""Result from `from_bounds` class method should be the same."""
507548
coordinates = [[(1.0, 2.0), (3.0, 2.0), (3.0, 4.0), (1.0, 4.0), (1.0, 2.0)]]

0 commit comments

Comments
 (0)