Skip to content
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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ Note: Minor version `0.X.0` update might break the API, It's recommended to pin

## [unreleased]

## [2.1.0] - 2025-10-08

* add `.create()` method to Geometry objects to create them without `type` key

```python
from geojson_pydantic import Point

Point.create(coordinates=(0,0))
>> Point(bbox=None, type='Point', coordinates=Position2D(longitude=0.0, latitude=0.0))
```

## [2.0.0] - 2025-05-05

* remove custom `__iter__`, `__getitem__` and `__len__` methods from `GeometryCollection` class **breaking change**
Expand Down
13 changes: 12 additions & 1 deletion docs/src/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ assert fc.features[0].properties["name"] == "jeff"
- `__geo_interface__`: GeoJSON-like protocol for geo-spatial (GIS) vector data ([spec](https://gist.github.com/sgillies/2217756#__geo_interface)).
- `has_z`: returns true if any coordinate has a Z value.
- `wkt`: returns the Well Known Text representation of the geometry.
- `create`: create a geometry object without providing the `type` information

##### For Polygon geometry

Expand Down Expand Up @@ -151,7 +152,7 @@ feat = MyPointFeatureModel(**geojson_feature)
assert feat.properties.name == "drew"
```

## Enforced Keys
## Enforced `type` Keys

Starting with version `0.6.0`, geojson-pydantic's classes will not define default keys such has `type`, `geometry` or `properties`.
This is to make sure the library does well its first goal, which is `validating` GeoJSON object based on the [specification](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.1)
Expand Down Expand Up @@ -187,3 +188,13 @@ Point(coordinates=(0,0))
Point(type="Point", coordinates=(0,0))
>> Point(type='Point', coordinates=(0.0, 0.0), bbox=None)
```

Starting with `2.1.0`, users can use the `.create()` methods to create geometries without the `type` information

```python
from geojson_pydantic import Point

Point.create(coordinates=(0,0))
# is equivalent to
Point(bbox=None, type='Point', coordinates=Position2D(longitude=0.0, latitude=0.0))
```
54 changes: 50 additions & 4 deletions geojson_pydantic/geometries.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from typing import Any, Iterator, List, Literal, Union

from pydantic import Field, field_validator
from typing_extensions import Annotated
from typing_extensions import Annotated, Self

from geojson_pydantic.base import _GeoJsonBase
from geojson_pydantic.types import (
Expand Down Expand Up @@ -105,6 +105,12 @@ def wkt(self) -> str:

return wkt

@classmethod
@abc.abstractmethod
def create(cls, **kwargs: Any) -> Self:
"""Create object from attributes."""
...


class Point(_GeometryBase):
"""Point Model"""
Expand All @@ -121,6 +127,12 @@ def has_z(self) -> bool:
"""Checks if any coordinate has a Z value."""
return _position_has_z(self.coordinates)

@classmethod
def create(cls, **kwargs: Any) -> Self:
"""Create object from attributes."""
t = kwargs.pop("type", "Point")
return cls(type=t, **kwargs)


class MultiPoint(_GeometryBase):
"""MultiPoint Model"""
Expand All @@ -140,6 +152,12 @@ def has_z(self) -> bool:
"""Checks if any coordinate has a Z value."""
return _position_list_has_z(self.coordinates)

@classmethod
def create(cls, **kwargs: Any) -> Self:
"""Create object from attributes."""
t = kwargs.pop("type", "MultiPoint")
return cls(type=t, **kwargs)


class LineString(_GeometryBase):
"""LineString Model"""
Expand All @@ -156,6 +174,12 @@ def has_z(self) -> bool:
"""Checks if any coordinate has a Z value."""
return _position_list_has_z(self.coordinates)

@classmethod
def create(cls, **kwargs: Any) -> Self:
"""Create object from attributes."""
t = kwargs.pop("type", "LineString")
return cls(type=t, **kwargs)


class MultiLineString(_GeometryBase):
"""MultiLineString Model"""
Expand All @@ -172,6 +196,12 @@ def has_z(self) -> bool:
"""Checks if any coordinate has a Z value."""
return _lines_has_z(self.coordinates)

@classmethod
def create(cls, **kwargs: Any) -> Self:
"""Create object from attributes."""
t = kwargs.pop("type", "MultiLineString")
return cls(type=t, **kwargs)


class Polygon(_GeometryBase):
"""Polygon Model"""
Expand Down Expand Up @@ -209,9 +239,7 @@ def has_z(self) -> bool:
return _lines_has_z(self.coordinates)

@classmethod
def from_bounds(
cls, xmin: float, ymin: float, xmax: float, ymax: float
) -> "Polygon":
def from_bounds(cls, xmin: float, ymin: float, xmax: float, ymax: float) -> Self:
"""Create a Polygon geometry from a boundingbox."""
return cls(
type="Polygon",
Expand All @@ -220,6 +248,12 @@ def from_bounds(
],
)

@classmethod
def create(cls, **kwargs: Any) -> Self:
"""Create object from attributes."""
t = kwargs.pop("type", "Polygon")
return cls(type=t, **kwargs)


class MultiPolygon(_GeometryBase):
"""MultiPolygon Model"""
Expand All @@ -244,6 +278,12 @@ def check_closure(cls, coordinates: List) -> List:

return coordinates

@classmethod
def create(cls, **kwargs: Any) -> Self:
"""Create object from attributes."""
t = kwargs.pop("type", "MultiPolygon")
return cls(type=t, **kwargs)


class GeometryCollection(_GeoJsonBase):
"""GeometryCollection Model"""
Expand Down Expand Up @@ -309,6 +349,12 @@ def check_geometries(cls, geometries: List) -> List:

return geometries

@classmethod
def create(cls, **kwargs: Any) -> Self:
"""Create object from attributes."""
t = kwargs.pop("type", "GeometryCollection")
return cls(type=t, **kwargs)


Geometry = Annotated[
Union[
Expand Down
128 changes: 128 additions & 0 deletions tests/test_geometries.py
Original file line number Diff line number Diff line change
Expand Up @@ -908,3 +908,131 @@ def test_geometry_collection_serializer():
assert "bbox" in geom_ser
assert "bbox" not in geom_ser["geometries"][0]
assert "bbox" not in geom_ser["geometries"][1]


@pytest.mark.parametrize(
"obj,kwargs",
(
(Point, {"coordinates": [0, 0], "bbox": [0, 0, 0, 0]}),
(Point, {"coordinates": [0, 0]}),
(Point, {"type": "Point", "coordinates": [0, 0]}),
(MultiPoint, {"coordinates": [(0.0, 0.0)], "bbox": [0, 0, 0, 0]}),
(MultiPoint, {"coordinates": [(0.0, 0.0)]}),
(MultiPoint, {"type": "MultiPoint", "coordinates": [(0.0, 0.0)]}),
(LineString, {"coordinates": [(0.0, 0.0), (1.0, 1.0)], "bbox": [0, 0, 1, 1]}),
(LineString, {"coordinates": [(0.0, 0.0), (1.0, 1.0)]}),
(LineString, {"type": "LineString", "coordinates": [(0.0, 0.0), (1.0, 1.0)]}),
(MultiLineString, {"coordinates": [[(0.0, 0.0), (1.0, 1.0)]]}),
(
MultiLineString,
{"coordinates": [[(0.0, 0.0), (1.0, 1.0)]], "bbox": [0, 0, 1, 1]},
),
(
MultiLineString,
{
"type": "MultiLineString",
"coordinates": [[(0.0, 0.0), (1.0, 1.0)]],
},
),
(
Polygon,
{
"coordinates": [[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]],
"bbox": [1.0, 2.0, 5.0, 6.0],
},
),
(Polygon, {"coordinates": [[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]]}),
(
Polygon,
{
"type": "Polygon",
"coordinates": [[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]],
},
),
(
MultiPolygon,
{
"coordinates": [[[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]]],
"bbox": [1.0, 2.0, 5.0, 6.0],
},
),
(
MultiPolygon,
{"coordinates": [[[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]]]},
),
(
MultiPolygon,
{
"type": "MultiPolygon",
"coordinates": [[[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]]],
},
),
(
GeometryCollection,
{
"geometries": [
{"type": "Point", "coordinates": [0, 0]},
{"type": "MultiPoint", "coordinates": [[1, 1]]},
]
},
),
(
GeometryCollection,
{
"type": "GeometryCollection",
"geometries": [
{"type": "Point", "coordinates": [0, 0]},
{"type": "MultiPoint", "coordinates": [[1, 1]]},
],
},
),
),
)
def test_geometry_create(obj, kwargs):
"""Test Geometry object create with new."""
assert obj.create(**kwargs)


@pytest.mark.parametrize(
"obj,kwargs",
(
(Point, {"type": "P", "coordinates": [0, 0]}),
(MultiPoint, {"type": "M", "coordinates": [(0.0, 0.0)]}),
(LineString, {"type": "L", "coordinates": [(0.0, 0.0), (1.0, 1.0)]}),
(
MultiLineString,
{
"type": "M",
"coordinates": [[(0.0, 0.0), (1.0, 1.0)]],
},
),
(
Polygon,
{
"type": "P",
"coordinates": [[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]],
},
),
(
MultiPolygon,
{
"type": "M",
"coordinates": [[[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]]],
},
),
(
GeometryCollection,
{
"type": "G",
"geometries": [
{"type": "Point", "coordinates": [0, 0]},
{"type": "MultiPoint", "coordinates": [[1, 1]]},
],
},
),
),
)
def test_geometry_new_invalid(obj, kwargs):
"""raise ValidationError with type is invalid."""
with pytest.raises(ValidationError):
obj.create(**kwargs)