diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index 5b2d46cb..5f5c3643 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -43,7 +43,7 @@ jobs: pip install -e ".[tests, lxml]" - name: Test with pytest run: | - pytest tests --cov=fastkml --cov=tests --cov-fail-under=88 --cov-report=xml + pytest tests --cov=fastkml --cov=tests --cov-fail-under=95 --cov-report=xml - name: "Upload coverage to Codecov" if: ${{ matrix.python-version==3.11 }} uses: codecov/codecov-action@v4 @@ -127,6 +127,9 @@ jobs: needs: [cpython, static-tests, pypy, cpython-lxml, doctest-lxml] name: Build and publish to PyPI and TestPyPI runs-on: ubuntu-latest + environment: release + permissions: + id-token: write steps: - uses: actions/checkout@v4 - name: Set up Python @@ -150,11 +153,8 @@ jobs: if: startsWith(github.ref, 'refs/tags') uses: pypa/gh-action-pypi-publish@release/v1 with: - password: ${{ secrets.TEST_PYPI_API_TOKEN }} repository_url: https://test.pypi.org/legacy/ - name: Publish distribution 📦 to PyPI for push to main if: github.event_name == 'push' && github.ref == 'refs/heads/main' uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.PYPI_API_TOKEN }} ... diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4016a47a..28995428 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,7 +40,7 @@ repos: rev: v3.17.0 hooks: - id: pyupgrade - args: ["--py3-plus", "--py37-plus"] + args: ["--py3-plus", "--py38-plus"] - repo: https://github.com/psf/black rev: 24.8.0 hooks: @@ -77,7 +77,7 @@ repos: rev: v1.11.2 hooks: - id: mypy - additional_dependencies: [pygeoif>=1.4, arrow, pytest] + additional_dependencies: [pygeoif>=1.4, arrow, pytest, hypothesis] - repo: https://github.com/adamchainz/blacken-docs rev: "1.18.0" hooks: diff --git a/docs/usage_guide.rst b/docs/usage_guide.rst index 1775894f..f04bf581 100644 --- a/docs/usage_guide.rst +++ b/docs/usage_guide.rst @@ -45,7 +45,7 @@ Example how to build a simple KML file from the Python interpreter. >>> f2.append(p) # Print out the KML Object as a string - >>> print(k.to_string(prettyprint=True)) + >>> print(k.to_string(prettyprint=True, precision=6)) doc name @@ -145,7 +145,7 @@ You can create a KML object by reading a KML file as a string >>> k.features[0].features[1].name = "ANOTHER NAME" # Verify that we can print back out the KML object as a string - >>> print(k.to_string(prettyprint=True)) + >>> print(k.to_string(prettyprint=True, precision=6)) Document.kml diff --git a/fastkml/features.py b/fastkml/features.py index 73cb809d..f001da73 100644 --- a/fastkml/features.py +++ b/fastkml/features.py @@ -185,6 +185,7 @@ def __bool__(self) -> bool: classes=(int,), get_kwarg=attribute_int_kwarg, set_element=int_attribute, + default=2, ), ) diff --git a/fastkml/geometry.py b/fastkml/geometry.py index aae7e04d..ef564372 100644 --- a/fastkml/geometry.py +++ b/fastkml/geometry.py @@ -93,6 +93,8 @@ MsgMutualExclusive: Final = "Geometry and kml coordinates are mutually exclusive" +xml_attrs = {"ns", "name_spaces", "id", "target_id"} + def handle_invalid_geometry_error( *, @@ -159,15 +161,14 @@ def coordinates_subelement( """ if getattr(obj, attr_name, None): - p = precision if precision is not None else 6 coords = getattr(obj, attr_name) - if len(coords[0]) == 2: # noqa: PLR2004 - tuples = (f"{c[0]:.{p}f},{c[1]:.{p}f}" for c in coords) - elif len(coords[0]) == 3: # noqa: PLR2004 - tuples = (f"{c[0]:.{p}f},{c[1]:.{p}f},{c[2]:.{p}f}" for c in coords) - else: + if not coords or len(coords[0]) not in (2, 3): msg = f"Invalid dimensions in coordinates '{coords}'" raise KMLWriteError(msg) + if precision is None: + tuples = (",".join(str(c) for c in coord) for coord in coords) + else: + tuples = (",".join(f"{c:.{precision}f}" for c in coord) for coord in coords) element.text = " ".join(tuples) @@ -488,6 +489,21 @@ def __bool__(self) -> bool: """ return bool(self.geometry) + def __eq__(self, other: object) -> bool: + """Check if the Point objects are equal.""" + if isinstance(other, Point): + return all( + getattr(self, attr) == getattr(other, attr) + for attr in ( + "extrude", + "altitude_mode", + "geometry", + *xml_attrs, + *self._get_splat(), + ) + ) + return super().__eq__(other) + @property def geometry(self) -> Optional[geo.Point]: """ @@ -628,6 +644,21 @@ def __bool__(self) -> bool: """ return bool(self.geometry) + def __eq__(self, other: object) -> bool: + """Check if the LineString objects is equal.""" + if isinstance(other, LineString): + return all( + getattr(self, attr) == getattr(other, attr) + for attr in ( + "extrude", + "tessellate", + "geometry", + *xml_attrs, + *self._get_splat(), + ) + ) + return super().__eq__(other) + @property def geometry(self) -> Optional[geo.LineString]: """ @@ -754,22 +785,6 @@ def __init__( **kwargs, ) - def __repr__(self) -> str: - """Create a string (c)representation for LinearRing.""" - return ( - f"{self.__class__.__module__}.{self.__class__.__name__}(" - f"ns={self.ns!r}, " - f"name_spaces={self.name_spaces!r}, " - f"id={self.id!r}, " - f"target_id={self.target_id!r}, " - f"extrude={self.extrude!r}, " - f"tessellate={self.tessellate!r}, " - f"altitude_mode={self.altitude_mode}, " - f"geometry={self.geometry!r}, " - f"**{self._get_splat()!r}," - ")" - ) - @property def geometry(self) -> Optional[geo.LinearRing]: """ @@ -1182,12 +1197,26 @@ def __repr__(self) -> str: f"extrude={self.extrude!r}, " f"tessellate={self.tessellate!r}, " f"altitude_mode={self.altitude_mode}, " - f"outer_boundary={self.outer_boundary!r}, " - f"inner_boundaries={self.inner_boundaries!r}, " + f"geometry={self.geometry!r}, " f"**{self._get_splat()!r}," ")" ) + def __eq__(self, other: object) -> bool: + """Check if the Polygon objects are equal.""" + if isinstance(other, Polygon): + return all( + getattr(self, attr) == getattr(other, attr) + for attr in ( + "extrude", + "tessellate", + "geometry", + *xml_attrs, + *self._get_splat(), + ) + ) + return super().__eq__(other) + registry.register( Polygon, diff --git a/pyproject.toml b/pyproject.toml index f841b26e..4a40b4af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,6 +77,7 @@ lxml = [ "lxml", ] tests = [ + "hypothesis", "pytest", "pytest-cov", ] @@ -117,6 +118,7 @@ source = [ [tool.coverage.report] exclude_also = [ "^\\s*\\.\\.\\.$", + "class \\w+\\(Protocol\\)\\:", "except AssertionError:", "except ImportError:", "if TYPE_CHECKING:", diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..b54dbc5a --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,21 @@ +""" +Configure the tests. + +Register the hypothesis 'exhaustive' profile to run 10 thousand examples. +Run this profile with ``pytest --hypothesis-profile=exhaustive`` +""" + +from hypothesis import HealthCheck +from hypothesis import settings + +settings.register_profile( + "exhaustive", + max_examples=10_000, + suppress_health_check=[HealthCheck.too_slow], +) +settings.register_profile( + "coverage", + max_examples=10, + suppress_health_check=[HealthCheck.too_slow], +) +settings.register_profile("ci", suppress_health_check=[HealthCheck.too_slow]) diff --git a/tests/geometries/boundaries_test.py b/tests/geometries/boundaries_test.py index 162fcd7c..dc7bec7a 100644 --- a/tests/geometries/boundaries_test.py +++ b/tests/geometries/boundaries_test.py @@ -37,7 +37,7 @@ def test_outer_boundary(self) -> None: ) assert outer_boundary.geometry == geo.LinearRing(coords) - assert outer_boundary.to_string(prettyprint=False).strip() == ( + assert outer_boundary.to_string(prettyprint=False, precision=6).strip() == ( '' "" "1.000000,2.000000 2.000000,0.000000 0.000000,0.000000 1.000000,2.000000" @@ -79,7 +79,7 @@ def test_inner_boundary(self) -> None: assert inner_boundary.geometry == geo.LinearRing(coords) assert bool(inner_boundary) - assert inner_boundary.to_string(prettyprint=False).strip() == ( + assert inner_boundary.to_string(prettyprint=False, precision=6).strip() == ( '' "" "1.000000,2.000000 2.000000,0.000000 0.000000,0.000000 1.000000,2.000000" diff --git a/tests/geometries/coordinates_test.py b/tests/geometries/coordinates_test.py index 9b8074e1..6b2e1b0e 100644 --- a/tests/geometries/coordinates_test.py +++ b/tests/geometries/coordinates_test.py @@ -29,7 +29,7 @@ def test_coordinates(self) -> None: coordinates = Coordinates(coords=coords) - assert coordinates.to_string().strip() == ( + assert coordinates.to_string(precision=6).strip() == ( '' "0.000000,0.000000 0.000000,1.000000 1.000000,1.000000 " "1.000000,0.000000 0.000000,0.000000" diff --git a/tests/geometries/geometry_test.py b/tests/geometries/geometry_test.py index 472e3bf5..a9781124 100644 --- a/tests/geometries/geometry_test.py +++ b/tests/geometries/geometry_test.py @@ -495,7 +495,7 @@ def test_create_kml_geometry_point(self) -> None: "coordinates": (0.0, 1.0), } assert "Point>" in g.to_string() - assert "coordinates>0.000000,1.0000000.000000,1.000000 None: """Test the create_kml_geometry function.""" @@ -509,7 +509,9 @@ def test_create_kml_geometry_linestring(self) -> None: "coordinates": ((0.0, 0.0), (1.0, 1.0)), } assert "LineString>" in g.to_string() - assert "coordinates>0.000000,0.000000 1.000000,1.0000000.000000,0.000000 1.000000,1.000000 None: """Test the create_kml_geometry function.""" @@ -526,7 +528,7 @@ def test_create_kml_geometry_linearring(self) -> None: assert ( "coordinates>0.000000,0.000000 1.000000,1.000000 1.000000,0.000000 " "0.000000,0.000000 None: """Test the create_kml_geometry function.""" @@ -543,7 +545,7 @@ def test_create_kml_geometry_polygon(self) -> None: assert ( "coordinates>0.000000,0.000000 1.000000,1.000000 1.000000,0.000000 " "0.000000,0.000000 None: """Test the create_kml_geometry function.""" @@ -552,12 +554,13 @@ def test_create_kml_geometry_multipoint(self) -> None: assert isinstance(g, MultiGeometry) assert g.geometry assert len(g.geometry) == 4 - assert "MultiGeometry>" in g.to_string() - assert "Point>" in g.to_string() - assert "coordinates>0.000000,0.0000001.000000,1.0000001.000000,0.0000002.000000,2.000000" in xml + assert "Point>" in xml + assert "coordinates>0.000000,0.0000001.000000,1.0000001.000000,0.0000002.000000,2.000000 None: """Test the create_kml_geometry function.""" @@ -568,10 +571,11 @@ def test_create_kml_geometry_multilinestring(self) -> None: assert isinstance(g, MultiGeometry) assert g.geometry assert len(g.geometry) == 2 - assert "MultiGeometry>" in g.to_string() - assert "LineString>" in g.to_string() - assert "coordinates>0.000000,0.000000 1.000000,1.0000000.000000,0.000000 1.000000,1.000000" in xml + assert "LineString>" in xml + assert "coordinates>0.000000,0.000000 1.000000,1.0000000.000000,0.000000 1.000000,1.000000 None: """Test the create_kml_geometry function.""" @@ -590,20 +594,21 @@ def test_create_kml_geometry_multipolygon(self) -> None: assert isinstance(g, MultiGeometry) assert g.geometry assert len(g.geometry) == 2 - assert "MultiGeometry>" in g.to_string() - assert "Polygon>" in g.to_string() + xml = g.to_string(precision=6) + assert "MultiGeometry>" in xml + assert "Polygon>" in xml assert ( "coordinates>0.000000,0.000000 0.000000,1.000000 1.000000,1.000000 " "1.000000,0.000000 0.000000,0.0000000.100000,0.100000 0.100000,0.200000 0.200000,0.200000 " "0.200000,0.100000 0.100000,0.1000000.000000,0.000000 0.000000,1.000000 1.000000,1.000000 " "1.000000,0.000000 0.000000,0.000000 None: multipoint = geo.MultiPoint([(0, 0), (1, 1), (1, 2), (2, 2)]) @@ -625,14 +630,15 @@ def test_create_kml_geometry_geometrycollection(self) -> None: assert isinstance(g, MultiGeometry) assert g.geometry assert len(g.geometry) == 7 - assert "MultiGeometry>" in g.to_string() - assert "LineString>" in g.to_string() - assert "LinearRing>" in g.to_string() - assert "Polygon>" in g.to_string() - assert "outerBoundaryIs>" in g.to_string() - assert "innerBoundaryIs>" in g.to_string() - assert "Point>" in g.to_string() - assert "coordinates>0.000000,0.000000" in xml + assert "LineString>" in xml + assert "LinearRing>" in xml + assert "Polygon>" in xml + assert "outerBoundaryIs>" in xml + assert "innerBoundaryIs>" in xml + assert "Point>" in xml + assert "coordinates>0.000000,0.000000 None: diff --git a/tests/geometries/linearring_test.py b/tests/geometries/linearring_test.py index bd3ff0a1..9966e8c0 100644 --- a/tests/geometries/linearring_test.py +++ b/tests/geometries/linearring_test.py @@ -45,7 +45,8 @@ def test_to_string(self) -> None: assert "LinearRing" in linear_ring.to_string() assert ( "coordinates>0.000000,0.000000 0.000000,1.000000 1.000000,1.000000 " - "1.000000,0.000000 0.000000,0.000000 None: diff --git a/tests/geometries/linestring_test.py b/tests/geometries/linestring_test.py index da9b079d..14afcbb4 100644 --- a/tests/geometries/linestring_test.py +++ b/tests/geometries/linestring_test.py @@ -55,7 +55,7 @@ def test_to_string(self) -> None: assert "LineString" in line_string.to_string() assert ( "coordinates>1.000000,2.000000 2.000000,0.000000 None: diff --git a/tests/geometries/multigeometry_test.py b/tests/geometries/multigeometry_test.py index d1d5dff2..9485a670 100644 --- a/tests/geometries/multigeometry_test.py +++ b/tests/geometries/multigeometry_test.py @@ -34,7 +34,7 @@ def test_1_point(self) -> None: mg = MultiGeometry(geometry=p) - assert "coordinates>1.000000,2.0000001.000000,2.000000" in mg.to_string() assert "Point>" in mg.to_string() @@ -44,8 +44,8 @@ def test_2_points(self) -> None: mg = MultiGeometry(geometry=p) - assert "coordinates>1.000000,2.0000003.000000,4.0000001.000000,2.0000003.000000,4.000000" in mg.to_string() assert "Point>" in mg.to_string() @@ -70,7 +70,9 @@ def test_1_linestring(self) -> None: mg = MultiGeometry(geometry=p) - assert "coordinates>1.000000,2.000000 3.000000,4.0000001.000000,2.000000 3.000000,4.000000" in mg.to_string() assert "LineString>" in mg.to_string() @@ -80,8 +82,12 @@ def test_2_linestrings(self) -> None: mg = MultiGeometry(geometry=p) - assert "coordinates>1.000000,2.000000 3.000000,4.0000005.000000,6.000000 7.000000,8.0000001.000000,2.000000 3.000000,4.0000005.000000,6.000000 7.000000,8.000000" in mg.to_string() assert "LineString>" in mg.to_string() @@ -111,7 +117,7 @@ def test_1_polygon(self) -> None: assert ( "coordinates>1.000000,2.000000 3.000000,4.000000 5.000000,6.000000 " - "1.000000,2.000000" in mg.to_string() assert "Polygon>" in mg.to_string() @@ -132,11 +138,11 @@ def test_1_polygons_with_holes(self) -> None: assert ( "coordinates>0.000000,0.000000 0.000000,1.000000 1.000000,1.000000 " - "1.000000,0.000000 0.000000,0.0000000.250000,0.250000 0.250000,0.500000 0.500000,0.500000 " - "0.500000,0.250000 0.250000,0.250000" in mg.to_string() assert "Polygon>" in mg.to_string() @@ -159,15 +165,15 @@ def test_2_polygons(self) -> None: assert ( "coordinates>0.000000,0.000000 0.000000,1.000000 1.000000,1.000000 " - "1.000000,0.000000 0.000000,0.0000000.100000,0.100000 0.100000,0.200000 0.200000,0.200000 " - "0.200000,0.100000 0.100000,0.1000000.000000,0.000000 0.000000,2.000000 1.000000,1.000000 " - "1.000000,0.000000 0.000000,0.000000" in mg.to_string() assert "Polygon>" in mg.to_string() @@ -214,7 +220,7 @@ def test_1_point(self) -> None: mg = MultiGeometry(geometry=p) - assert "coordinates>1.000000,2.0000001.000000,2.000000" in mg.to_string() assert "Point>" in mg.to_string() diff --git a/tests/geometries/point_test.py b/tests/geometries/point_test.py index 2a4a2859..348db869 100644 --- a/tests/geometries/point_test.py +++ b/tests/geometries/point_test.py @@ -56,7 +56,7 @@ def test_to_string_2d(self) -> None: point = Point(geometry=p) assert "Point" in point.to_string() - assert "coordinates>1.000000,2.0000001.000000,2.000000 None: """Test the to_string method.""" @@ -65,7 +65,9 @@ def test_to_string_3d(self) -> None: point = Point(geometry=p) assert "Point" in point.to_string() - assert "coordinates>1.000000,2.000000,3.0000001.000000,2.000000,3.000000 None: """Test the to_string method, exclude default for extrude in terse mode.""" diff --git a/tests/geometries/polygon_test.py b/tests/geometries/polygon_test.py index 283daa6b..5ceb7eb9 100644 --- a/tests/geometries/polygon_test.py +++ b/tests/geometries/polygon_test.py @@ -43,7 +43,7 @@ def test_exterior_only(self) -> None: assert ( "0.000000,0.000000 0.000000,1.000000 1.000000,1.000000 " "1.000000,0.000000 0.000000,0.000000" - ) in polygon.to_string() + ) in polygon.to_string(precision=6) def test_exterior_interior(self) -> None: """Test exterior and interior.""" @@ -60,11 +60,11 @@ def test_exterior_interior(self) -> None: assert ( "0.000000,0.000000 0.000000,1.000000 1.000000,1.000000 " "1.000000,0.000000 0.000000,0.000000" - ) in polygon.to_string() + ) in polygon.to_string(precision=6) assert ( "0.100000,0.100000 0.100000,0.900000 0.900000,0.900000 " "0.900000,0.100000 0.100000,0.100000" - ) in polygon.to_string() + ) in polygon.to_string(precision=6) def test_exterior_interior_tessellate_extrude_altitude_mode(self) -> None: """ diff --git a/tests/hypothesis/__init__.py b/tests/hypothesis/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/hypothesis/geometry_test.py b/tests/hypothesis/geometry_test.py new file mode 100644 index 00000000..9a4f7570 --- /dev/null +++ b/tests/hypothesis/geometry_test.py @@ -0,0 +1,469 @@ +# Copyright (C) 2024 Christian Ledermann +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +"""Property based tests of the Geometry classes.""" +import string +import typing +from functools import partial + +from hypothesis import given +from hypothesis import strategies as st +from pygeoif.geometry import LinearRing +from pygeoif.geometry import LineString +from pygeoif.geometry import Point +from pygeoif.geometry import Polygon +from pygeoif.hypothesis.strategies import epsg4326 +from pygeoif.hypothesis.strategies import line_coords +from pygeoif.hypothesis.strategies import line_strings +from pygeoif.hypothesis.strategies import points +from pygeoif.hypothesis.strategies import polygons + +import fastkml.geometry +from fastkml.enums import AltitudeMode +from fastkml.enums import Verbosity + +eval_locals = { + "Point": Point, + "Polygon": Polygon, + "LineString": LineString, + "LinearRing": LinearRing, + "AltitudeMode": AltitudeMode, + "fastkml": fastkml, +} + +ID_TEXT = string.ascii_letters + string.digits + string.punctuation +kml_geometry = typing.Union[ + fastkml.geometry.Point, + fastkml.geometry.LineString, + fastkml.geometry.Polygon, +] + +coordinates = partial( + given, + coords=st.one_of(st.none(), line_coords(srs=epsg4326, min_points=1)), +) + +common_geometry = partial( + given, + id=st.one_of(st.none(), st.text(alphabet=ID_TEXT)), + target_id=st.one_of(st.none(), st.text(ID_TEXT)), + extrude=st.one_of(st.none(), st.booleans()), + tessellate=st.one_of(st.none(), st.booleans()), + altitude_mode=st.one_of(st.none(), st.sampled_from(AltitudeMode)), +) + + +def _test_repr_roundtrip(geometry: kml_geometry) -> None: + new_g = eval(repr(geometry), {}, eval_locals) # noqa: S307 + + assert geometry == new_g + + +def _test_geometry_str_roundtrip(geometry: kml_geometry) -> None: + new_g = type(geometry).class_from_string(geometry.to_string()) + + assert geometry.to_string() == new_g.to_string() + assert geometry == new_g + + +def _test_geometry_str_roundtrip_terse(geometry: kml_geometry) -> None: + new_g = type(geometry).class_from_string( + geometry.to_string(verbosity=Verbosity.terse), + ) + + assert geometry.to_string(verbosity=Verbosity.verbose) == new_g.to_string( + verbosity=Verbosity.verbose, + ) + assert geometry.geometry == new_g.geometry + + if geometry.altitude_mode == AltitudeMode.clamp_to_ground: + assert new_g.altitude_mode is None + else: + assert new_g.altitude_mode == geometry.altitude_mode + if geometry.extrude: + assert new_g.extrude is True + else: + assert new_g.extrude is None + if hasattr(geometry, "tessellate"): + assert not isinstance(geometry, fastkml.geometry.Point) + if geometry.tessellate: + assert new_g.tessellate is True + else: + assert new_g.tessellate is None + + +def _test_geometry_str_roundtrip_verbose(geometry: kml_geometry) -> None: + new_g = type(geometry).class_from_string( + geometry.to_string(verbosity=Verbosity.verbose), + ) + + assert geometry.to_string(verbosity=Verbosity.terse) == new_g.to_string( + verbosity=Verbosity.terse, + ) + assert geometry.geometry == new_g.geometry + assert new_g.altitude_mode is not None + if geometry.altitude_mode is None: + assert new_g.altitude_mode == AltitudeMode.clamp_to_ground + if geometry.extrude is None: + assert new_g.extrude is False + else: + assert new_g.extrude == geometry.extrude + if hasattr(geometry, "tessellate"): + assert not isinstance(geometry, fastkml.geometry.Point) + if geometry.tessellate is None: + assert new_g.tessellate is False + else: + assert new_g.tessellate == geometry.tessellate + + +@coordinates() +def test_coordinates_str_roundtrip( + coords: typing.Union[ + typing.Sequence[typing.Tuple[float, float]], + typing.Sequence[typing.Tuple[float, float, float]], + None, + ], +) -> None: + coordinate = fastkml.geometry.Coordinates(coords=coords) + + new_c = fastkml.geometry.Coordinates.class_from_string( + coordinate.to_string(precision=20), + ) + + assert coordinate.to_string(precision=10) == new_c.to_string(precision=10) + + +@coordinates() +def test_coordinates_repr_roundtrip( + coords: typing.Union[ + typing.Sequence[typing.Tuple[float, float]], + typing.Sequence[typing.Tuple[float, float, float]], + None, + ], +) -> None: + coordinate = fastkml.geometry.Coordinates(coords=coords) + + new_c = eval(repr(coordinate), {}, eval_locals) # noqa: S307 + + assert coordinate == new_c + + +@common_geometry( + geometry=st.one_of( + st.none(), + points(srs=epsg4326), + ), +) +def test_point_repr_roundtrip( + id: typing.Optional[str], + target_id: typing.Optional[str], + extrude: typing.Optional[bool], + altitude_mode: typing.Optional[AltitudeMode], + tessellate: typing.Optional[bool], # noqa: ARG001 + geometry: typing.Optional[Point], +) -> None: + point = fastkml.geometry.Point( + id=id, + target_id=target_id, + extrude=extrude, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_repr_roundtrip(point) + + +@common_geometry( + geometry=st.one_of( + st.none(), + points(srs=epsg4326), + ), +) +def test_point_str_roundtrip( + id: typing.Optional[str], + target_id: typing.Optional[str], + extrude: typing.Optional[bool], + tessellate: typing.Optional[bool], # noqa: ARG001 + altitude_mode: typing.Optional[AltitudeMode], + geometry: typing.Optional[Point], +) -> None: + point = fastkml.geometry.Point( + id=id, + target_id=target_id, + extrude=extrude, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip(point) + + +@common_geometry( + geometry=st.one_of( + st.none(), + points(srs=epsg4326), + ), +) +def test_point_str_roundtrip_terse( + id: typing.Optional[str], + target_id: typing.Optional[str], + extrude: typing.Optional[bool], + tessellate: typing.Optional[bool], # noqa: ARG001 + altitude_mode: typing.Optional[AltitudeMode], + geometry: typing.Optional[Point], +) -> None: + point = fastkml.geometry.Point( + id=id, + target_id=target_id, + extrude=extrude, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip_terse(point) + + +@common_geometry( + geometry=st.one_of( + st.none(), + points(srs=epsg4326), + ), +) +def test_point_str_roundtrip_verbose( + id: typing.Optional[str], + target_id: typing.Optional[str], + extrude: typing.Optional[bool], + tessellate: typing.Optional[bool], # noqa: ARG001 + altitude_mode: typing.Optional[AltitudeMode], + geometry: typing.Optional[Point], +) -> None: + point = fastkml.geometry.Point( + id=id, + target_id=target_id, + extrude=extrude, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip_verbose(point) + + +@common_geometry( + geometry=st.one_of( + st.none(), + line_strings(srs=epsg4326), + ), +) +def test_linestring_repr_roundtrip( + id: typing.Optional[str], + target_id: typing.Optional[str], + extrude: typing.Optional[bool], + tessellate: typing.Optional[bool], + altitude_mode: typing.Optional[AltitudeMode], + geometry: typing.Optional[LineString], +) -> None: + line = fastkml.geometry.LineString( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_repr_roundtrip(line) + + +@common_geometry( + geometry=st.one_of( + st.none(), + line_strings(srs=epsg4326), + ), +) +def test_linestring_str_roundtrip( + id: typing.Optional[str], + target_id: typing.Optional[str], + extrude: typing.Optional[bool], + tessellate: typing.Optional[bool], + altitude_mode: typing.Optional[AltitudeMode], + geometry: typing.Optional[LineString], +) -> None: + line = fastkml.geometry.LineString( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip(line) + + +@common_geometry( + geometry=st.one_of( + st.none(), + line_strings(srs=epsg4326), + ), +) +def test_linestring_str_roundtrip_terse( + id: typing.Optional[str], + target_id: typing.Optional[str], + extrude: typing.Optional[bool], + tessellate: typing.Optional[bool], + altitude_mode: typing.Optional[AltitudeMode], + geometry: typing.Optional[LineString], +) -> None: + line = fastkml.geometry.LineString( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip_terse(line) + + +@common_geometry( + geometry=st.one_of( + st.none(), + line_strings(srs=epsg4326), + ), +) +def test_linestring_str_roundtrip_verbose( + id: typing.Optional[str], + target_id: typing.Optional[str], + extrude: typing.Optional[bool], + tessellate: typing.Optional[bool], + altitude_mode: typing.Optional[AltitudeMode], + geometry: typing.Optional[LineString], +) -> None: + line = fastkml.geometry.LineString( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip_verbose(line) + + +@common_geometry( + geometry=st.one_of( + st.none(), + polygons(srs=epsg4326), + ), +) +def test_polygon_repr_roundtrip( + id: typing.Optional[str], + target_id: typing.Optional[str], + extrude: typing.Optional[bool], + tessellate: typing.Optional[bool], + altitude_mode: typing.Optional[AltitudeMode], + geometry: typing.Optional[Polygon], +) -> None: + polygon = fastkml.geometry.Polygon( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_repr_roundtrip(polygon) + + +@common_geometry( + geometry=st.one_of( + st.none(), + polygons(srs=epsg4326), + ), +) +def test_polygon_str_roundtrip( + id: typing.Optional[str], + target_id: typing.Optional[str], + extrude: typing.Optional[bool], + tessellate: typing.Optional[bool], + altitude_mode: typing.Optional[AltitudeMode], + geometry: typing.Optional[Polygon], +) -> None: + polygon = fastkml.geometry.Polygon( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip(polygon) + + +@common_geometry( + geometry=st.one_of( + st.none(), + polygons(srs=epsg4326), + ), +) +def test_polygon_str_roundtrip_terse( + id: typing.Optional[str], + target_id: typing.Optional[str], + extrude: typing.Optional[bool], + tessellate: typing.Optional[bool], + altitude_mode: typing.Optional[AltitudeMode], + geometry: typing.Optional[Polygon], +) -> None: + polygon = fastkml.geometry.Polygon( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip_terse(polygon) + + +@common_geometry( + geometry=st.one_of( + st.none(), + polygons(srs=epsg4326), + ), +) +def test_polygon_str_roundtrip_verbose( + id: typing.Optional[str], + target_id: typing.Optional[str], + extrude: typing.Optional[bool], + tessellate: typing.Optional[bool], + altitude_mode: typing.Optional[AltitudeMode], + geometry: typing.Optional[Polygon], +) -> None: + polygon = fastkml.geometry.Polygon( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip_verbose(polygon) diff --git a/tests/hypothesis/multi_geometry_test.py b/tests/hypothesis/multi_geometry_test.py new file mode 100644 index 00000000..144f8963 --- /dev/null +++ b/tests/hypothesis/multi_geometry_test.py @@ -0,0 +1,681 @@ +# Copyright (C) 2024 Christian Ledermann +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +"""Property based tests of the Geometry classes.""" +from __future__ import annotations + +import string +from functools import partial + +from hypothesis import given +from hypothesis import strategies as st +from pygeoif.geometry import GeometryCollection +from pygeoif.geometry import LinearRing +from pygeoif.geometry import LineString +from pygeoif.geometry import MultiLineString +from pygeoif.geometry import MultiPoint +from pygeoif.geometry import MultiPolygon +from pygeoif.geometry import Point +from pygeoif.geometry import Polygon +from pygeoif.hypothesis.strategies import epsg4326 +from pygeoif.hypothesis.strategies import geometry_collections +from pygeoif.hypothesis.strategies import multi_line_strings +from pygeoif.hypothesis.strategies import multi_points +from pygeoif.hypothesis.strategies import multi_polygons + +import fastkml.geometry +from fastkml.enums import AltitudeMode +from fastkml.enums import Verbosity + +eval_locals = { + "Point": Point, + "Polygon": Polygon, + "LineString": LineString, + "LinearRing": LinearRing, + "AltitudeMode": AltitudeMode, + "MultiPoint": MultiPoint, + "MultiLineString": MultiLineString, + "MultiPolygon": MultiPolygon, + "GeometryCollection": GeometryCollection, + "fastkml": fastkml, +} + +ID_TEXT = string.ascii_letters + string.digits + string.punctuation + +common_geometry = partial( + given, + id=st.one_of(st.none(), st.text(alphabet=ID_TEXT)), + target_id=st.one_of(st.none(), st.text(ID_TEXT)), + extrude=st.one_of(st.none(), st.booleans()), + tessellate=st.one_of(st.none(), st.booleans()), + altitude_mode=st.one_of(st.none(), st.sampled_from(AltitudeMode)), +) + + +def _test_repr_roundtrip( + geometry: fastkml.geometry.MultiGeometry, + cls: type[MultiPoint | MultiLineString | MultiPolygon | GeometryCollection], +) -> None: + new_g = eval(repr(geometry), {}, eval_locals) # noqa: S307 + + assert geometry == new_g + if geometry: + assert type(new_g.geometry) is cls + + +def _test_geometry_str_roundtrip( + geometry: fastkml.geometry.MultiGeometry, + *, + cls: type[MultiPoint | MultiLineString | MultiPolygon], + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, +) -> None: + new_g = fastkml.geometry.MultiGeometry.class_from_string(geometry.to_string()) + + assert geometry.to_string() == new_g.to_string() + assert geometry == new_g + if not geometry: + return + assert new_g.geometry + assert geometry.geometry + assert type(new_g.geometry) is cls + for g1, g2 in zip(new_g.kml_geometries, geometry.kml_geometries): + assert g1.extrude == g2.extrude == extrude + assert g1.altitude_mode == g2.altitude_mode == altitude_mode + if not isinstance(g1, fastkml.geometry.Point): + assert g1.tessellate == g2.tessellate == tessellate + + +def _test_geometry_str_roundtrip_terse( + geometry: fastkml.geometry.MultiGeometry, + *, + cls: type[MultiPoint | MultiLineString | MultiPolygon], + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, +) -> None: + new_g = fastkml.geometry.MultiGeometry.class_from_string( + geometry.to_string(verbosity=Verbosity.terse), + ) + + assert geometry.to_string(verbosity=Verbosity.verbose) == new_g.to_string( + verbosity=Verbosity.verbose, + ) + if not geometry: + return + assert new_g.geometry + assert geometry.geometry + assert type(new_g.geometry) is cls + for new, orig in zip(new_g.kml_geometries, geometry.kml_geometries): + if extrude: + assert new.extrude == orig.extrude == extrude + else: + assert new.extrude is None + if altitude_mode == AltitudeMode.clamp_to_ground: + assert new.altitude_mode is None + else: + assert new.altitude_mode == orig.altitude_mode == altitude_mode + if not isinstance(new, fastkml.geometry.Point): + if tessellate: + assert new.tessellate == orig.tessellate == tessellate + else: + assert new.tessellate is None + + +def _test_geometry_str_roundtrip_verbose( + geometry: fastkml.geometry.MultiGeometry, + *, + cls: type[MultiPoint | MultiLineString | MultiPolygon], + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, +) -> None: + new_g = fastkml.geometry.MultiGeometry.class_from_string( + geometry.to_string(verbosity=Verbosity.verbose), + ) + + assert geometry.to_string(verbosity=Verbosity.terse) == new_g.to_string( + verbosity=Verbosity.terse, + ) + if not geometry: + return + assert new_g.geometry + assert geometry.geometry + assert type(new_g.geometry) is cls + for new, orig in zip(new_g.kml_geometries, geometry.kml_geometries): + if extrude: + assert new.extrude == orig.extrude == extrude + else: + assert new.extrude is False + if altitude_mode is None: + assert new.altitude_mode == AltitudeMode.clamp_to_ground + else: + assert new.altitude_mode == orig.altitude_mode == altitude_mode + if not isinstance(new, fastkml.geometry.Point): + if tessellate: + assert new.tessellate == orig.tessellate == tessellate + else: + assert new.tessellate is False + + +@common_geometry( + geometry=st.one_of( + st.none(), + multi_points(srs=epsg4326), + ), +) +def test_multipoint_repr_roundtrip( + id: str | None, + target_id: str | None, + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, + geometry: MultiPoint | None, +) -> None: + multi_geometry = fastkml.geometry.MultiGeometry( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_repr_roundtrip(multi_geometry, MultiPoint) + + +@common_geometry( + geometry=st.one_of( + st.none(), + multi_points(srs=epsg4326), + ), +) +def test_multipoint_str_roundtrip( + id: str | None, + target_id: str | None, + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, + geometry: MultiPoint | None, +) -> None: + multi_geometry = fastkml.geometry.MultiGeometry( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip( + multi_geometry, + cls=MultiPoint, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + ) + + +@common_geometry( + geometry=st.one_of( + st.none(), + multi_points(srs=epsg4326), + ), +) +def test_multipoint_str_roundtrip_terse( + id: str | None, + target_id: str | None, + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, + geometry: MultiPoint | None, +) -> None: + multi_geometry = fastkml.geometry.MultiGeometry( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip_terse( + multi_geometry, + cls=MultiPoint, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + ) + + +@common_geometry( + geometry=st.one_of( + st.none(), + multi_points(srs=epsg4326), + ), +) +def test_multipoint_str_roundtrip_verbose( + id: str | None, + target_id: str | None, + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, + geometry: MultiPoint | None, +) -> None: + multi_geometry = fastkml.geometry.MultiGeometry( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip_verbose( + multi_geometry, + cls=MultiPoint, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + ) + + +@common_geometry( + geometry=st.one_of( + st.none(), + multi_line_strings(srs=epsg4326), + ), +) +def test_multilinestring_repr_roundtrip( + id: str | None, + target_id: str | None, + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, + geometry: MultiLineString | None, +) -> None: + multi_geometry = fastkml.geometry.MultiGeometry( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_repr_roundtrip(multi_geometry, MultiLineString) + + +@common_geometry( + geometry=st.one_of( + st.none(), + multi_line_strings(srs=epsg4326), + ), +) +def test_multilinestring_str_roundtrip( + id: str | None, + target_id: str | None, + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, + geometry: MultiLineString | None, +) -> None: + multi_geometry = fastkml.geometry.MultiGeometry( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip( + multi_geometry, + cls=MultiLineString, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + ) + + +@common_geometry( + geometry=st.one_of( + st.none(), + multi_line_strings(srs=epsg4326), + ), +) +def test_multilinestring_str_roundtrip_terse( + id: str | None, + target_id: str | None, + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, + geometry: MultiLineString | None, +) -> None: + multi_geometry = fastkml.geometry.MultiGeometry( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip_terse( + multi_geometry, + cls=MultiLineString, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + ) + + +@common_geometry( + geometry=st.one_of( + st.none(), + multi_line_strings(srs=epsg4326), + ), +) +def test_multilinestring_str_roundtrip_verbose( + id: str | None, + target_id: str | None, + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, + geometry: MultiLineString | None, +) -> None: + multi_geometry = fastkml.geometry.MultiGeometry( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip_verbose( + multi_geometry, + cls=MultiLineString, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + ) + + +@common_geometry( + geometry=st.one_of( + st.none(), + multi_polygons(srs=epsg4326), + ), +) +def test_multipolygon_repr_roundtrip( + id: str | None, + target_id: str | None, + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, + geometry: MultiPolygon | None, +) -> None: + multi_geometry = fastkml.geometry.MultiGeometry( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_repr_roundtrip(multi_geometry, MultiPolygon) + + +@common_geometry( + geometry=st.one_of( + st.none(), + multi_polygons(srs=epsg4326), + ), +) +def test_multipolygon_str_roundtrip( + id: str | None, + target_id: str | None, + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, + geometry: MultiPolygon | None, +) -> None: + multi_geometry = fastkml.geometry.MultiGeometry( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip( + multi_geometry, + cls=MultiPolygon, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + ) + + +@common_geometry( + geometry=st.one_of( + st.none(), + multi_polygons(srs=epsg4326), + ), +) +def test_multipolygon_str_roundtrip_terse( + id: str | None, + target_id: str | None, + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, + geometry: MultiPolygon | None, +) -> None: + multi_geometry = fastkml.geometry.MultiGeometry( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip_terse( + multi_geometry, + cls=MultiPolygon, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + ) + + +@common_geometry( + geometry=st.one_of( + st.none(), + multi_polygons(srs=epsg4326), + ), +) +def test_multipolygon_str_roundtrip_verbose( + id: str | None, + target_id: str | None, + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, + geometry: MultiPolygon | None, +) -> None: + multi_geometry = fastkml.geometry.MultiGeometry( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip_verbose( + multi_geometry, + cls=MultiPolygon, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + ) + + +@common_geometry( + geometry=st.one_of( + st.none(), + geometry_collections(srs=epsg4326), + ), +) +def test_geometrycollection_repr_roundtrip( + id: str | None, + target_id: str | None, + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, + geometry: GeometryCollection | None, +) -> None: + multi_geometry = fastkml.geometry.MultiGeometry( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + new_mg = eval(repr(multi_geometry), {}, eval_locals) # noqa: S307 + + assert multi_geometry == new_mg + if geometry: + assert isinstance( + new_mg.geometry, + (GeometryCollection, MultiPolygon, MultiLineString, MultiPoint), + ) + else: + assert not new_mg + + +@common_geometry( + geometry=st.one_of( + st.none(), + geometry_collections(srs=epsg4326), + ), +) +def test_geometrycollection_str_roundtrip( + id: str | None, + target_id: str | None, + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, + geometry: GeometryCollection | None, +) -> None: + multi_geometry = fastkml.geometry.MultiGeometry( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + new_mg = fastkml.geometry.MultiGeometry.class_from_string( + multi_geometry.to_string(), + ) + + if geometry: + assert isinstance( + new_mg.geometry, + (GeometryCollection, MultiPolygon, MultiLineString, MultiPoint), + ) + else: + assert not new_mg + + +@common_geometry( + geometry=st.one_of( + st.none(), + geometry_collections(srs=epsg4326), + ), +) +def test_geometrycollection_str_roundtrip_terse( + id: str | None, + target_id: str | None, + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, + geometry: GeometryCollection | None, +) -> None: + multi_geometry = fastkml.geometry.MultiGeometry( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + new_mg = fastkml.geometry.MultiGeometry.class_from_string( + multi_geometry.to_string(verbosity=Verbosity.terse), + ) + + if geometry: + assert isinstance( + new_mg.geometry, + (GeometryCollection, MultiPolygon, MultiLineString, MultiPoint), + ) + else: + assert not new_mg + + +@common_geometry( + geometry=st.one_of( + st.none(), + geometry_collections(srs=epsg4326), + ), +) +def test_geometrycollection_str_roundtrip_verbose( + id: str | None, + target_id: str | None, + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, + geometry: GeometryCollection | None, +) -> None: + multi_geometry = fastkml.geometry.MultiGeometry( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + new_mg = fastkml.geometry.MultiGeometry.class_from_string( + multi_geometry.to_string(verbosity=Verbosity.verbose), + ) + + if geometry: + assert isinstance( + new_mg.geometry, + (GeometryCollection, MultiPolygon, MultiLineString, MultiPoint), + ) + else: + assert not new_mg