developmentseed / geojson-pydantic

Pydantic data models for the GeoJSON spec
https://developmentseed.org/geojson-pydantic/
MIT License
220 stars 34 forks source link

Test failures with GEOS 3.12 #139

Closed sebastic closed 1 year ago

sebastic commented 1 year ago

The Debian package build fails with GEOS 3.12:

I: pybuild base:240: cd /build/geojson-pydantic-0.6.3/.pybuild/cpython3_3.11_geojson-pydantic/build; python3.11 -m pytest tests
============================= test session starts ==============================
platform linux -- Python 3.11.4, pytest-7.4.0, pluggy-1.0.0+repack
rootdir: /build/geojson-pydantic-0.6.3/.pybuild/cpython3_3.11_geojson-pydantic/build
collected 134 items

tests/test_features.py ....................                              [ 14%]
tests/test_geometries.py ............FFFF....................F.......... [ 50%]
.F........F.......................................................       [ 99%]
tests/test_package.py .                                                  [100%]

=================================== FAILURES ===================================
_______________ test_multi_point_valid_coordinates[coordinates1] _______________

coordinates = [(1.0, 2.0)]

    @pytest.mark.parametrize(
        "coordinates",
        [
            # Empty array
            [],
            # No Z
            [(1.0, 2.0)],
            [(1.0, 2.0), (1.0, 2.0)],
            # Has Z
            [(1.0, 2.0, 3.0), (1.0, 2.0, 3.0)],
            # Mixed
            [(1.0, 2.0), (1.0, 2.0, 3.0)],
        ],
    )
    def test_multi_point_valid_coordinates(coordinates):
        """
        Two or three number elements as coordinates should be okay, as well as an empty array.
        """
        p = MultiPoint(type="MultiPoint", coordinates=coordinates)
        assert p.type == "MultiPoint"
        assert p.coordinates == coordinates
        assert hasattr(p, "__geo_interface__")
>       assert_wkt_equivalence(p)

tests/test_geometries.py:74: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

geom = MultiPoint(type='MultiPoint', coordinates=[(1.0, 2.0)], bbox=None)

    def assert_wkt_equivalence(geom: Union[Geometry, GeometryCollection]):
        """Assert WKT equivalence with Shapely."""
        # Remove any trailing `.0` to match Shapely format
        clean_wkt = re.sub(r"\.0(\D)", r"\1", geom.wkt)
>       assert shape(geom).wkt == clean_wkt
E       AssertionError: assert 'MULTIPOINT ((1 2))' == 'MULTIPOINT (1 2)'
E         - MULTIPOINT (1 2)
E         + MULTIPOINT ((1 2))
E         ?            +     +

tests/test_geometries.py:25: AssertionError
_______________ test_multi_point_valid_coordinates[coordinates2] _______________

coordinates = [(1.0, 2.0), (1.0, 2.0)]

    @pytest.mark.parametrize(
        "coordinates",
        [
            # Empty array
            [],
            # No Z
            [(1.0, 2.0)],
            [(1.0, 2.0), (1.0, 2.0)],
            # Has Z
            [(1.0, 2.0, 3.0), (1.0, 2.0, 3.0)],
            # Mixed
            [(1.0, 2.0), (1.0, 2.0, 3.0)],
        ],
    )
    def test_multi_point_valid_coordinates(coordinates):
        """
        Two or three number elements as coordinates should be okay, as well as an empty array.
        """
        p = MultiPoint(type="MultiPoint", coordinates=coordinates)
        assert p.type == "MultiPoint"
        assert p.coordinates == coordinates
        assert hasattr(p, "__geo_interface__")
>       assert_wkt_equivalence(p)

tests/test_geometries.py:74: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

geom = MultiPoint(type='MultiPoint', coordinates=[(1.0, 2.0), (1.0, 2.0)], bbox=None)

    def assert_wkt_equivalence(geom: Union[Geometry, GeometryCollection]):
        """Assert WKT equivalence with Shapely."""
        # Remove any trailing `.0` to match Shapely format
        clean_wkt = re.sub(r"\.0(\D)", r"\1", geom.wkt)
>       assert shape(geom).wkt == clean_wkt
E       AssertionError: assert 'MULTIPOINT ((1 2), (1 2))' == 'MULTIPOINT (1 2, 1 2)'
E         - MULTIPOINT (1 2, 1 2)
E         + MULTIPOINT ((1 2), (1 2))

tests/test_geometries.py:25: AssertionError
_______________ test_multi_point_valid_coordinates[coordinates3] _______________

coordinates = [(1.0, 2.0, 3.0), (1.0, 2.0, 3.0)]

    @pytest.mark.parametrize(
        "coordinates",
        [
            # Empty array
            [],
            # No Z
            [(1.0, 2.0)],
            [(1.0, 2.0), (1.0, 2.0)],
            # Has Z
            [(1.0, 2.0, 3.0), (1.0, 2.0, 3.0)],
            # Mixed
            [(1.0, 2.0), (1.0, 2.0, 3.0)],
        ],
    )
    def test_multi_point_valid_coordinates(coordinates):
        """
        Two or three number elements as coordinates should be okay, as well as an empty array.
        """
        p = MultiPoint(type="MultiPoint", coordinates=coordinates)
        assert p.type == "MultiPoint"
        assert p.coordinates == coordinates
        assert hasattr(p, "__geo_interface__")
>       assert_wkt_equivalence(p)

tests/test_geometries.py:74: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

geom = MultiPoint(type='MultiPoint', coordinates=[(1.0, 2.0, 3.0), (1.0, 2.0, 3.0)], bbox=None)

    def assert_wkt_equivalence(geom: Union[Geometry, GeometryCollection]):
        """Assert WKT equivalence with Shapely."""
        # Remove any trailing `.0` to match Shapely format
        clean_wkt = re.sub(r"\.0(\D)", r"\1", geom.wkt)
>       assert shape(geom).wkt == clean_wkt
E       AssertionError: assert 'MULTIPOINT Z... 3), (1 2 3))' == 'MULTIPOINT Z (1 2 3, 1 2 3)'
E         - MULTIPOINT Z (1 2 3, 1 2 3)
E         + MULTIPOINT Z ((1 2 3), (1 2 3))

tests/test_geometries.py:25: AssertionError
_______________ test_multi_point_valid_coordinates[coordinates4] _______________

coordinates = [(1.0, 2.0), (1.0, 2.0, 3.0)]

    @pytest.mark.parametrize(
        "coordinates",
        [
            # Empty array
            [],
            # No Z
            [(1.0, 2.0)],
            [(1.0, 2.0), (1.0, 2.0)],
            # Has Z
            [(1.0, 2.0, 3.0), (1.0, 2.0, 3.0)],
            # Mixed
            [(1.0, 2.0), (1.0, 2.0, 3.0)],
        ],
    )
    def test_multi_point_valid_coordinates(coordinates):
        """
        Two or three number elements as coordinates should be okay, as well as an empty array.
        """
        p = MultiPoint(type="MultiPoint", coordinates=coordinates)
        assert p.type == "MultiPoint"
        assert p.coordinates == coordinates
        assert hasattr(p, "__geo_interface__")
>       assert_wkt_equivalence(p)

tests/test_geometries.py:74: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

geom = MultiPoint(type='MultiPoint', coordinates=[(1.0, 2.0), (1.0, 2.0, 3.0)], bbox=None)

    def assert_wkt_equivalence(geom: Union[Geometry, GeometryCollection]):
        """Assert WKT equivalence with Shapely."""
        # Remove any trailing `.0` to match Shapely format
        clean_wkt = re.sub(r"\.0(\D)", r"\1", geom.wkt)
>       assert shape(geom).wkt == clean_wkt
E       AssertionError: assert 'MULTIPOINT Z...aN), (1 2 3))' == 'MULTIPOINT Z (1 2 0, 1 2 3)'
E         - MULTIPOINT Z (1 2 0, 1 2 3)
E         ?                   ^
E         + MULTIPOINT Z ((1 2 NaN), (1 2 3))
E         ?              +     ^^^^  +      +

tests/test_geometries.py:25: AssertionError
____________ test_multi_line_string_valid_coordinates[coordinates6] ____________

coordinates = [[(1.0, 2.0), (3.0, 4.0)], [(0.0, 0.0, 0.0), (1.0, 1.0, 1.0)]]

    @pytest.mark.parametrize(
        "coordinates",
        [
            # Empty array
            [],
            # One line, two points, no Z
            [[(1.0, 2.0), (3.0, 4.0)]],
            # One line, two points, has Z
            [[(0.0, 0.0, 0.0), (1.0, 1.0, 1.0)]],
            # One line, three points, no Z
            [[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0)]],
            # Two lines, two points each, no Z
            [[(1.0, 2.0), (3.0, 4.0)], [(0.0, 0.0), (1.0, 1.0)]],
            # Two lines, two points each, has Z
            [[(1.0, 2.0, 0.0), (3.0, 4.0, 1.0)], [(0.0, 0.0, 0.0), (1.0, 1.0, 1.0)]],
            # Mixed
            [[(1.0, 2.0), (3.0, 4.0)], [(0.0, 0.0, 0.0), (1.0, 1.0, 1.0)]],
        ],
    )
    def test_multi_line_string_valid_coordinates(coordinates):
        """
        A list of two coordinates or more should be okay
        """
        multilinestring = MultiLineString(type="MultiLineString", coordinates=coordinates)
        assert multilinestring.type == "MultiLineString"
        assert multilinestring.coordinates == coordinates
        assert hasattr(multilinestring, "__geo_interface__")
>       assert_wkt_equivalence(multilinestring)

tests/test_geometries.py:148: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

geom = MultiLineString(type='MultiLineString', coordinates=[[(1.0, 2.0), (3.0, 4.0)], [(0.0, 0.0, 0.0), (1.0, 1.0, 1.0)]], bbox=None)

    def assert_wkt_equivalence(geom: Union[Geometry, GeometryCollection]):
        """Assert WKT equivalence with Shapely."""
        # Remove any trailing `.0` to match Shapely format
        clean_wkt = re.sub(r"\.0(\D)", r"\1", geom.wkt)
>       assert shape(geom).wkt == clean_wkt
E       AssertionError: assert 'MULTILINESTR... 0 0, 1 1 1))' == 'MULTILINESTR... 0 0, 1 1 1))'
E         - MULTILINESTRING Z ((1 2 0, 3 4 0), (0 0 0, 1 1 1))
E         + MULTILINESTRING Z ((1 2 NaN, 3 4 NaN), (0 0 0, 1 1 1))

tests/test_geometries.py:25: AssertionError
____________________ test_polygon_with_holes[coordinates2] _____________________

coordinates = [[(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0), (0.0, 0.0)], [(2.0, 2.0, 2.0), (2.0, 4.0, 0.0), (4.0, 4.0, 0.0), (4.0, 2.0, 0.0), (2.0, 2.0, 2.0)]]

    @pytest.mark.parametrize(
        "coordinates",
        [
            # Polygon with holes, no Z
            [
                [(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0), (0.0, 0.0)],
                [(2.0, 2.0), (2.0, 4.0), (4.0, 4.0), (4.0, 2.0), (2.0, 2.0)],
            ],
            # Polygon with holes, has Z
            [
                [
                    (0.0, 0.0, 0.0),
                    (0.0, 10.0, 0.0),
                    (10.0, 10.0, 0.0),
                    (10.0, 0.0, 0.0),
                    (0.0, 0.0, 0.0),
                ],
                [
                    (2.0, 2.0, 1.0),
                    (2.0, 4.0, 1.0),
                    (4.0, 4.0, 1.0),
                    (4.0, 2.0, 1.0),
                    (2.0, 2.0, 1.0),
                ],
            ],
            # Mixed
            [
                [(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0), (0.0, 0.0)],
                [
                    (2.0, 2.0, 2.0),
                    (2.0, 4.0, 0.0),
                    (4.0, 4.0, 0.0),
                    (4.0, 2.0, 0.0),
                    (2.0, 2.0, 2.0),
                ],
            ],
        ],
    )
    def test_polygon_with_holes(coordinates):
        """Check interior and exterior rings."""
        polygon = Polygon(type="Polygon", coordinates=coordinates)
        assert polygon.type == "Polygon"
        assert hasattr(polygon, "__geo_interface__")
        assert polygon.exterior == polygon.coordinates[0]
        assert list(polygon.interiors) == [polygon.coordinates[1]]
>       assert_wkt_equivalence(polygon)

tests/test_geometries.py:234: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

geom = Polygon(type='Polygon', coordinates=[[(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0), (0.0, 0.0)], [(2.0, 2.0, 2.0), (2.0, 4.0, 0.0), (4.0, 4.0, 0.0), (4.0, 2.0, 0.0), (2.0, 2.0, 2.0)]], bbox=None)

    def assert_wkt_equivalence(geom: Union[Geometry, GeometryCollection]):
        """Assert WKT equivalence with Shapely."""
        # Remove any trailing `.0` to match Shapely format
        clean_wkt = re.sub(r"\.0(\D)", r"\1", geom.wkt)
>       assert shape(geom).wkt == clean_wkt
E       AssertionError: assert 'POLYGON Z ((... 2 0, 2 2 2))' == 'POLYGON Z ((... 2 0, 2 2 2))'
E         - POLYGON Z ((0 0 0, 0 10 0, 10 10 0, 10 0 0, 0 0 0), (2 2 2, 2 4 0, 4 4 0, 4 2 0, 2 2 2))
E         + POLYGON Z ((0 0 NaN, 0 10 NaN, 10 10 NaN, 10 0 NaN, 0 0 NaN), (2 2 2, 2 4 0, 4 4 0, 4 2 0, 2 2 2))

tests/test_geometries.py:25: AssertionError
_______________________ test_multi_polygon[coordinates3] _______________________

coordinates = [[[(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (0.0, 0.0)], [(2.1, 2.1, 2.1), (2.2, 2.1, 2.0), (2.2, 2.2, 2.2), (2.1, 2.2, 2.3), (2.1, 2.1, 2.1)]]]

    @pytest.mark.parametrize(
        "coordinates",
        [
            # Empty array
            [],
            # Multipolygon, no Z
            [
                [
                    [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (0.0, 0.0)],
                    [(2.1, 2.1), (2.2, 2.1), (2.2, 2.2), (2.1, 2.2), (2.1, 2.1)],
                ]
            ],
            # Multipolygon, has Z
            [
                [
                    [
                        (0.0, 0.0, 4.0),
                        (1.0, 0.0, 4.0),
                        (1.0, 1.0, 4.0),
                        (0.0, 1.0, 4.0),
                        (0.0, 0.0, 4.0),
                    ],
                    [
                        (2.1, 2.1, 4.0),
                        (2.2, 2.1, 4.0),
                        (2.2, 2.2, 4.0),
                        (2.1, 2.2, 4.0),
                        (2.1, 2.1, 4.0),
                    ],
                ]
            ],
            # Mixed
            [
                [
                    [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (0.0, 0.0)],
                    [
                        (2.1, 2.1, 2.1),
                        (2.2, 2.1, 2.0),
                        (2.2, 2.2, 2.2),
                        (2.1, 2.2, 2.3),
                        (2.1, 2.1, 2.1),
                    ],
                ]
            ],
        ],
    )
    def test_multi_polygon(coordinates):
        """Should accept sequence of polygons."""
        multi_polygon = MultiPolygon(type="MultiPolygon", coordinates=coordinates)

        assert multi_polygon.type == "MultiPolygon"
        assert hasattr(multi_polygon, "__geo_interface__")
>       assert_wkt_equivalence(multi_polygon)

tests/test_geometries.py:311: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

geom = MultiPolygon(type='MultiPolygon', coordinates=[[[(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (0.0, 0.0)], [(2.1, 2.1, 2.1), (2.2, 2.1, 2.0), (2.2, 2.2, 2.2), (2.1, 2.2, 2.3), (2.1, 2.1, 2.1)]]], bbox=None)

    def assert_wkt_equivalence(geom: Union[Geometry, GeometryCollection]):
        """Assert WKT equivalence with Shapely."""
        # Remove any trailing `.0` to match Shapely format
        clean_wkt = re.sub(r"\.0(\D)", r"\1", geom.wkt)
>       assert shape(geom).wkt == clean_wkt
E       AssertionError: assert 'MULTIPOLYGON....1 2.1 2.1)))' == 'MULTIPOLYGON....1 2.1 2.1)))'
E         - MULTIPOLYGON Z (((0 0 0, 1 0 0, 1 1 0, 0 1 0, 0 0 0), (2.1 2.1 2.1, 2.2 2.1 2, 2.2 2.2 2.2, 2.1 2.2 2.3, 2.1 2.1 2.1)))
E         ?                               -----  ^^^^^^^^^^^^^^
E         + MULTIPOLYGON Z (((0 0 NaN, 1 0 NaN, 1 1 NaN, 0 1 NaN, 0 0 NaN), (2.1 2.1 2.1, 2.2 2.1 2, 2.2 2.2 2.2, 2.1 2.2 2.3, 2.1 2.1 2.1)))
E         ?                       +++++++ ++++    +++++++  +++++++   ^^^^

tests/test_geometries.py:25: AssertionError
=========================== short test summary info ============================
FAILED tests/test_geometries.py::test_multi_point_valid_coordinates[coordinates1]
FAILED tests/test_geometries.py::test_multi_point_valid_coordinates[coordinates2]
FAILED tests/test_geometries.py::test_multi_point_valid_coordinates[coordinates3]
FAILED tests/test_geometries.py::test_multi_point_valid_coordinates[coordinates4]
FAILED tests/test_geometries.py::test_multi_line_string_valid_coordinates[coordinates6]
FAILED tests/test_geometries.py::test_polygon_with_holes[coordinates2] - Asse...
FAILED tests/test_geometries.py::test_multi_polygon[coordinates3] - Assertion...
======================== 7 failed, 127 passed in 0.79s =========================
E: pybuild pybuild:388: test: plugin pyproject failed with: exit code=1: cd /build/geojson-pydantic-0.6.3/.pybuild/cpython3_3.11_geojson-pydantic/build; python3.11 -m pytest tests
eseglem commented 1 year ago

Looks like Geos 3.12 changed two things that break these tests.

They added parentheses in MultiPoint, which is technically correct. Very surprised this wasn't already the case. Easy enough to adjust that part. The change also broke a Shapely test. https://github.com/shapely/shapely/pull/1820 They now have a test that checks based on version, but not sure how we go about that without a bunch of regex replacement which probably defeats the purpose of testing. Might be worth just testing against static strings vs Shapely / Geos. Then we don't break again when things like this happen. It is nice to try and match up to what they do though, so there is some purpose to keeping it.

And I guess they flipped from 0 to NaN when adding a third dimension? Not sure what is up with that, still need to dig through where that change is and see what the logic is. I think this may be a Geos issue though, as NaN shouldn't be valid in WKT.

Figuring out the best way to test everything is the hard part.

vincentsarago commented 1 year ago

we could do the same as they do (skip if geos version is too low) (https://github.com/shapely/shapely/pull/1820/files#diff-dd888b20a0fd5d6ecbff7de3e85aa28070ebc9a80be24711eeb57f4eaf69d2c3R326-R329)

I think it's nice to have a validation using shapely

And I guess they flipped from 0 to NaN when adding a third dimension? Not sure what is up with that, still need to dig through where that change is and see what the logic is. I think this may be a Geos issue though, as NaN shouldn't be valid in WKT.

maybe linked to https://github.com/shapely/shapely/issues/1524#issuecomment-1239340672 🤔