From eca9807244db94f9e2d916d5fed8819edc8e15c7 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 5 Oct 2024 15:48:22 +0100 Subject: [PATCH 01/15] Add hypothesis tests for coordinates --- .pre-commit-config.yaml | 4 +- fastkml/features.py | 1 + tests/hypothesis/__init__.py | 0 tests/hypothesis/geometry_test.py | 75 +++++++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 tests/hypothesis/__init__.py create mode 100644 tests/hypothesis/geometry_test.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 53344162..155d3059 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/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/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..7b6c9bb9 --- /dev/null +++ b/tests/hypothesis/geometry_test.py @@ -0,0 +1,75 @@ +# 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 typing + +from hypothesis import given +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 + +import fastkml.geometry +from fastkml.enums import AltitudeMode + +eval_locals = { + "Point": Point, + "Polygon": Polygon, + "LineString": LineString, + "LinearRing": LinearRing, + "AltitudeMode": AltitudeMode, + "fastkml": fastkml, +} + + +@given( + coords=line_coords(srs=epsg4326, min_points=1), +) +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) + + +@given( + coords=line_coords(srs=epsg4326, min_points=1), +) +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 From cb6bd775f65a4c286f7600fa67b218e9a8f9ac7c Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 5 Oct 2024 16:00:07 +0100 Subject: [PATCH 02/15] add hypothesis to test requirements --- .github/workflows/run-all-tests.yml | 2 +- pyproject.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index 5b2d46cb..564e3ae2 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 diff --git a/pyproject.toml b/pyproject.toml index f841b26e..7f3c66ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,6 +77,7 @@ lxml = [ "lxml", ] tests = [ + "hypothesis", "pytest", "pytest-cov", ] From aa035802844f8c3b036858650e820d708a0ee25f Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 5 Oct 2024 16:46:20 +0100 Subject: [PATCH 03/15] add tests for Point --- tests/hypothesis/geometry_test.py | 141 +++++++++++++++++++++++++++++- 1 file changed, 140 insertions(+), 1 deletion(-) diff --git a/tests/hypothesis/geometry_test.py b/tests/hypothesis/geometry_test.py index 7b6c9bb9..42b67d83 100644 --- a/tests/hypothesis/geometry_test.py +++ b/tests/hypothesis/geometry_test.py @@ -15,19 +15,22 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA """Property based tests of the Geometry classes.""" - +import string import typing 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 points import fastkml.geometry from fastkml.enums import AltitudeMode +from fastkml.enums import Verbosity eval_locals = { "Point": Point, @@ -38,6 +41,8 @@ "fastkml": fastkml, } +ID_TEXT = string.ascii_letters + string.digits + string.punctuation + @given( coords=line_coords(srs=epsg4326, min_points=1), @@ -73,3 +78,137 @@ def test_coordinates_repr_roundtrip( new_c = eval(repr(coordinate), {}, eval_locals) # noqa: S307 assert coordinate == new_c + + +@given( + id=st.one_of(st.none(), st.text(alphabet=string.printable)), + target_id=st.one_of(st.none(), st.text(alphabet=string.printable)), + extrude=st.one_of(st.none(), st.booleans()), + altitude_mode=st.one_of(st.none(), st.sampled_from(fastkml.enums.AltitudeMode)), + 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], + geometry: typing.Optional[Point], +) -> None: + point = fastkml.geometry.Point( + id=id, + target_id=target_id, + extrude=extrude, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + new_p = eval(repr(point), {}, eval_locals) # noqa: S307 + + assert point == new_p + + +@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()), + altitude_mode=st.one_of(st.none(), st.sampled_from(AltitudeMode)), + 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], + 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, + ) + + new_p = fastkml.geometry.Point.class_from_string(point.to_string(precision=15)) + + assert point.to_string() == new_p.to_string() + + +@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()), + altitude_mode=st.one_of(st.none(), st.sampled_from(AltitudeMode)), + 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], + 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, + ) + + new_p = fastkml.geometry.Point.class_from_string( + point.to_string(precision=15, verbosity=Verbosity.terse), + ) + + assert (new_p.altitude_mode is None) or ( + new_p.altitude_mode != AltitudeMode.clamp_to_ground + ) + assert (new_p.extrude is None) or (new_p.extrude is True) + assert point.to_string(verbosity=Verbosity.verbose) == new_p.to_string( + verbosity=Verbosity.verbose, + ) + + +@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()), + altitude_mode=st.one_of(st.none(), st.sampled_from(AltitudeMode)), + 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], + 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, + ) + + new_p = fastkml.geometry.Point.class_from_string( + point.to_string(precision=15, verbosity=Verbosity.verbose), + ) + + assert isinstance(new_p.altitude_mode, AltitudeMode) + assert isinstance(new_p.extrude, bool) + assert point.to_string(verbosity=Verbosity.terse) == new_p.to_string( + verbosity=Verbosity.terse, + ) From c21b1fcb0ed1707c3e4ea20c15be7450e4f54521 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 5 Oct 2024 18:35:39 +0100 Subject: [PATCH 04/15] Exclude protocols from coverage --- pyproject.toml | 1 + tests/conftest.py | 21 +++++++++++++++++++++ tests/hypothesis/geometry_test.py | 2 +- 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 tests/conftest.py diff --git a/pyproject.toml b/pyproject.toml index 7f3c66ad..4a40b4af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -118,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/hypothesis/geometry_test.py b/tests/hypothesis/geometry_test.py index 42b67d83..f5502cda 100644 --- a/tests/hypothesis/geometry_test.py +++ b/tests/hypothesis/geometry_test.py @@ -84,7 +84,7 @@ def test_coordinates_repr_roundtrip( id=st.one_of(st.none(), st.text(alphabet=string.printable)), target_id=st.one_of(st.none(), st.text(alphabet=string.printable)), extrude=st.one_of(st.none(), st.booleans()), - altitude_mode=st.one_of(st.none(), st.sampled_from(fastkml.enums.AltitudeMode)), + altitude_mode=st.one_of(st.none(), st.sampled_from(AltitudeMode)), geometry=st.one_of( st.none(), points(srs=epsg4326), From 7acfb302512e7bca2a4ff4b06026c5255b9192b6 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sun, 6 Oct 2024 20:36:52 +0100 Subject: [PATCH 05/15] update release to trusted publisher --- .github/workflows/run-all-tests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index 564e3ae2..5f5c3643 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -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 }} ... From 78774c51be174737d3fe3f23fd52ee7188429676 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 12 Oct 2024 17:41:00 +0100 Subject: [PATCH 06/15] Refactor coordinate precision --- fastkml/geometry.py | 9 ++-- tests/geometries/boundaries_test.py | 4 +- tests/geometries/coordinates_test.py | 2 +- tests/geometries/geometry_test.py | 60 ++++++++++++++------------ tests/geometries/linearring_test.py | 3 +- tests/geometries/linestring_test.py | 2 +- tests/geometries/multigeometry_test.py | 32 ++++++++------ tests/geometries/point_test.py | 6 ++- tests/geometries/polygon_test.py | 6 +-- tests/hypothesis/geometry_test.py | 11 +++-- 10 files changed, 76 insertions(+), 59 deletions(-) diff --git a/fastkml/geometry.py b/fastkml/geometry.py index aae7e04d..c311984b 100644 --- a/fastkml/geometry.py +++ b/fastkml/geometry.py @@ -159,13 +159,12 @@ 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) + 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) + if len(coords[0]) not in (2, 3): msg = f"Invalid dimensions in coordinates '{coords}'" raise KMLWriteError(msg) element.text = " ".join(tuples) diff --git a/tests/geometries/boundaries_test.py b/tests/geometries/boundaries_test.py index 4b413f08..3f63df75 100644 --- a/tests/geometries/boundaries_test.py +++ b/tests/geometries/boundaries_test.py @@ -35,7 +35,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" @@ -66,7 +66,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 037b572f..b945aa56 100644 --- a/tests/geometries/linestring_test.py +++ b/tests/geometries/linestring_test.py @@ -47,7 +47,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 7d9b7783..1e7e4795 100644 --- a/tests/geometries/multigeometry_test.py +++ b/tests/geometries/multigeometry_test.py @@ -32,7 +32,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() @@ -42,8 +42,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() @@ -68,7 +68,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() @@ -78,8 +80,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() @@ -109,7 +115,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() @@ -130,11 +136,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() @@ -157,15 +163,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() @@ -212,7 +218,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 178a3c6b..8a81cfeb 100644 --- a/tests/geometries/point_test.py +++ b/tests/geometries/point_test.py @@ -46,7 +46,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.""" @@ -55,7 +55,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 df21b984..ebd4f9eb 100644 --- a/tests/geometries/polygon_test.py +++ b/tests/geometries/polygon_test.py @@ -41,7 +41,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.""" @@ -58,11 +58,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/geometry_test.py b/tests/hypothesis/geometry_test.py index f5502cda..09691f83 100644 --- a/tests/hypothesis/geometry_test.py +++ b/tests/hypothesis/geometry_test.py @@ -135,9 +135,10 @@ def test_point_str_roundtrip( geometry=geometry, ) - new_p = fastkml.geometry.Point.class_from_string(point.to_string(precision=15)) + new_p = fastkml.geometry.Point.class_from_string(point.to_string()) assert point.to_string() == new_p.to_string() + assert point.geometry == new_p.geometry @given( @@ -173,8 +174,9 @@ def test_point_str_roundtrip_terse( new_p.altitude_mode != AltitudeMode.clamp_to_ground ) assert (new_p.extrude is None) or (new_p.extrude is True) - assert point.to_string(verbosity=Verbosity.verbose) == new_p.to_string( + assert point.to_string(verbosity=Verbosity.verbose, precision=6) == new_p.to_string( verbosity=Verbosity.verbose, + precision=6, ) @@ -204,11 +206,12 @@ def test_point_str_roundtrip_verbose( ) new_p = fastkml.geometry.Point.class_from_string( - point.to_string(precision=15, verbosity=Verbosity.verbose), + point.to_string(verbosity=Verbosity.verbose), ) assert isinstance(new_p.altitude_mode, AltitudeMode) assert isinstance(new_p.extrude, bool) - assert point.to_string(verbosity=Verbosity.terse) == new_p.to_string( + assert point.to_string(verbosity=Verbosity.terse, precision=16) == new_p.to_string( verbosity=Verbosity.terse, + precision=16, ) From 4dbdb9e0e5b50b4170121f50c0bdf3d5851fa48b Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 12 Oct 2024 18:23:09 +0100 Subject: [PATCH 07/15] Refactor geometry classes and add equality checks --- fastkml/geometry.py | 66 ++++++++++++++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 18 deletions(-) diff --git a/fastkml/geometry.py b/fastkml/geometry.py index c311984b..7158bc0c 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( *, @@ -487,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]: """ @@ -627,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]: """ @@ -753,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]: """ @@ -1181,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, From 6e8db13fecd0586639417be4240394c6718facab Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 12 Oct 2024 18:43:12 +0100 Subject: [PATCH 08/15] Refactor geometry tests and add LineString roundtrip tests --- tests/hypothesis/geometry_test.py | 142 ++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/tests/hypothesis/geometry_test.py b/tests/hypothesis/geometry_test.py index 09691f83..8d967591 100644 --- a/tests/hypothesis/geometry_test.py +++ b/tests/hypothesis/geometry_test.py @@ -26,6 +26,7 @@ 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 import fastkml.geometry @@ -215,3 +216,144 @@ def test_point_str_roundtrip_verbose( verbosity=Verbosity.terse, precision=16, ) + + +@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)), + 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, + ) + + new_l = eval(repr(line), {}, eval_locals) # noqa: S307 + + assert line == new_l + + +@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)), + 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, + ) + + new_l = fastkml.geometry.LineString.class_from_string(line.to_string()) + + assert line.to_string() == new_l.to_string() + assert line == new_l + + +@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)), + 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, + ) + + new_l = fastkml.geometry.LineString.class_from_string( + line.to_string(verbosity=Verbosity.terse), + ) + + assert line.to_string(verbosity=Verbosity.verbose) == new_l.to_string( + verbosity=Verbosity.verbose, + ) + + +@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)), + 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, + ) + + new_l = fastkml.geometry.LineString.class_from_string( + line.to_string(verbosity=Verbosity.verbose), + ) + + assert line.to_string(verbosity=Verbosity.terse) == new_l.to_string( + verbosity=Verbosity.terse, + ) From 9edd464bfed4d7f81aa491395884213dbfc0eb1e Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 12 Oct 2024 18:54:32 +0100 Subject: [PATCH 09/15] add hypothesis tests for polygon --- tests/hypothesis/geometry_test.py | 142 ++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/tests/hypothesis/geometry_test.py b/tests/hypothesis/geometry_test.py index 8d967591..5e52e776 100644 --- a/tests/hypothesis/geometry_test.py +++ b/tests/hypothesis/geometry_test.py @@ -28,6 +28,7 @@ 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 @@ -357,3 +358,144 @@ def test_linestring_str_roundtrip_verbose( assert line.to_string(verbosity=Verbosity.terse) == new_l.to_string( verbosity=Verbosity.terse, ) + + +@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)), + 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, + ) + + new_p = eval(repr(polygon), {}, eval_locals) # noqa: S307 + + assert polygon == new_p + + +@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)), + 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, + ) + + new_p = fastkml.geometry.Polygon.class_from_string(polygon.to_string()) + + assert polygon.to_string() == new_p.to_string() + assert polygon == new_p + + +@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)), + 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, + ) + + new_p = fastkml.geometry.Polygon.class_from_string( + polygon.to_string(verbosity=Verbosity.terse), + ) + + assert polygon.to_string(verbosity=Verbosity.verbose) == new_p.to_string( + verbosity=Verbosity.verbose, + ) + + +@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)), + 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, + ) + + new_p = fastkml.geometry.Polygon.class_from_string( + polygon.to_string(verbosity=Verbosity.verbose), + ) + + assert polygon.to_string(verbosity=Verbosity.verbose) == new_p.to_string( + verbosity=Verbosity.verbose, + ) From b73fcc5d0419eb6e2f6cff2a8a36759a1d4f2969 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 12 Oct 2024 20:12:26 +0100 Subject: [PATCH 10/15] add test for MultiGeometries --- tests/hypothesis/multi_geometry_test.py | 545 ++++++++++++++++++++++++ 1 file changed, 545 insertions(+) create mode 100644 tests/hypothesis/multi_geometry_test.py diff --git a/tests/hypothesis/multi_geometry_test.py b/tests/hypothesis/multi_geometry_test.py new file mode 100644 index 00000000..3c71e560 --- /dev/null +++ b/tests/hypothesis/multi_geometry_test.py @@ -0,0 +1,545 @@ +# 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 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 + + +@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)), + geometry=st.one_of( + st.none(), + multi_points(srs=epsg4326), + ), +) +def test_multipoint_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[MultiPoint], +) -> 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, MultiPoint) + + +@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)), + geometry=st.one_of( + st.none(), + multi_points(srs=epsg4326), + ), +) +def test_multipoint_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[MultiPoint], +) -> 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(), + ) + + assert multi_geometry.to_string() == new_mg.to_string() + assert multi_geometry == new_mg + if geometry: + assert isinstance(new_mg.geometry, MultiPoint) + + +@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)), + geometry=st.one_of( + st.none(), + multi_points(srs=epsg4326), + ), +) +def test_multipoint_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[MultiPoint], +) -> 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), + ) + + assert multi_geometry.to_string(verbosity=Verbosity.verbose) == new_mg.to_string( + verbosity=Verbosity.verbose, + ) + if geometry: + assert isinstance(new_mg.geometry, MultiPoint) + + +@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)), + geometry=st.one_of( + st.none(), + multi_points(srs=epsg4326), + ), +) +def test_multipoint_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[MultiPoint], +) -> 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), + ) + + assert multi_geometry.to_string(verbosity=Verbosity.verbose) == new_mg.to_string( + verbosity=Verbosity.verbose, + ) + if geometry: + assert isinstance(new_mg.geometry, MultiPoint) + + +@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)), + geometry=st.one_of( + st.none(), + multi_line_strings(srs=epsg4326), + ), +) +def test_multilinestring_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[MultiLineString], +) -> 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, MultiLineString) + + +@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)), + geometry=st.one_of( + st.none(), + multi_line_strings(srs=epsg4326), + ), +) +def test_multilinestring_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[MultiLineString], +) -> 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(), + ) + + assert multi_geometry.to_string() == new_mg.to_string() + assert multi_geometry == new_mg + if geometry: + assert isinstance(new_mg.geometry, MultiLineString) + + +@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)), + geometry=st.one_of( + st.none(), + multi_line_strings(srs=epsg4326), + ), +) +def test_multilinestring_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[MultiLineString], +) -> 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), + ) + + assert multi_geometry.to_string(verbosity=Verbosity.verbose) == new_mg.to_string( + verbosity=Verbosity.verbose, + ) + if geometry: + assert isinstance(new_mg.geometry, MultiLineString) + + +@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)), + geometry=st.one_of( + st.none(), + multi_line_strings(srs=epsg4326), + ), +) +def test_multilinestring_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[MultiLineString], +) -> 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), + ) + + assert multi_geometry.to_string(verbosity=Verbosity.verbose) == new_mg.to_string( + verbosity=Verbosity.verbose, + ) + if geometry: + assert isinstance(new_mg.geometry, MultiLineString) + + +@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)), + geometry=st.one_of( + st.none(), + multi_polygons(srs=epsg4326), + ), +) +def test_multipolygon_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[MultiPolygon], +) -> 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, MultiPolygon) + + +@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)), + geometry=st.one_of( + st.none(), + multi_polygons(srs=epsg4326), + ), +) +def test_multipolygon_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[MultiPolygon], +) -> 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(), + ) + + assert multi_geometry.to_string() == new_mg.to_string() + assert multi_geometry == new_mg + if geometry: + assert isinstance(new_mg.geometry, MultiPolygon) + + +@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)), + geometry=st.one_of( + st.none(), + multi_polygons(srs=epsg4326), + ), +) +def test_multipolygon_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[MultiPolygon], +) -> 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), + ) + + assert multi_geometry.to_string(verbosity=Verbosity.verbose) == new_mg.to_string( + verbosity=Verbosity.verbose, + ) + if geometry: + assert isinstance(new_mg.geometry, MultiPolygon) + + +@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)), + geometry=st.one_of( + st.none(), + multi_polygons(srs=epsg4326), + ), +) +def test_multipolygon_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[MultiPolygon], +) -> 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), + ) + + assert multi_geometry.to_string(verbosity=Verbosity.verbose) == new_mg.to_string( + verbosity=Verbosity.verbose, + ) + if geometry: + assert isinstance(new_mg.geometry, MultiPolygon) + + +@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)), + geometry=st.one_of( + st.none(), + geometry_collections(srs=epsg4326), + ), +) +def test_geometrycollection_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[GeometryCollection], +) -> 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), + ) From 8aab9e6d873f288551f62d268d970e6c56e4aa81 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 12 Oct 2024 20:18:45 +0100 Subject: [PATCH 11/15] test multigeometry geometrycollection --- tests/hypothesis/multi_geometry_test.py | 125 ++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/tests/hypothesis/multi_geometry_test.py b/tests/hypothesis/multi_geometry_test.py index 3c71e560..19950372 100644 --- a/tests/hypothesis/multi_geometry_test.py +++ b/tests/hypothesis/multi_geometry_test.py @@ -543,3 +543,128 @@ def test_geometrycollection_repr_roundtrip( new_mg.geometry, (GeometryCollection, MultiPolygon, MultiLineString, MultiPoint), ) + else: + assert not new_mg + + +@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)), + geometry=st.one_of( + st.none(), + geometry_collections(srs=epsg4326), + ), +) +def test_geometrycollection_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[GeometryCollection], +) -> 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 + + +@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)), + geometry=st.one_of( + st.none(), + geometry_collections(srs=epsg4326), + ), +) +def test_geometrycollection_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[GeometryCollection], +) -> 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 + + +@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)), + geometry=st.one_of( + st.none(), + geometry_collections(srs=epsg4326), + ), +) +def test_geometrycollection_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[GeometryCollection], +) -> 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 From 8b5f36a566f1f6d78b49ecbe59e5fbf272d0f522 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sun, 13 Oct 2024 13:24:14 +0100 Subject: [PATCH 12/15] Refactor hypothesis tests to avoid code duplication --- fastkml/geometry.py | 6 +- tests/hypothesis/geometry_test.py | 258 +++++++++++------------- tests/hypothesis/multi_geometry_test.py | 114 ++--------- 3 files changed, 138 insertions(+), 240 deletions(-) diff --git a/fastkml/geometry.py b/fastkml/geometry.py index 7158bc0c..ef564372 100644 --- a/fastkml/geometry.py +++ b/fastkml/geometry.py @@ -162,13 +162,13 @@ def coordinates_subelement( """ if getattr(obj, attr_name, None): coords = getattr(obj, attr_name) + 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) - if len(coords[0]) not in (2, 3): - msg = f"Invalid dimensions in coordinates '{coords}'" - raise KMLWriteError(msg) element.text = " ".join(tuples) diff --git a/tests/hypothesis/geometry_test.py b/tests/hypothesis/geometry_test.py index 5e52e776..9a4f7570 100644 --- a/tests/hypothesis/geometry_test.py +++ b/tests/hypothesis/geometry_test.py @@ -17,6 +17,7 @@ """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 @@ -44,11 +45,91 @@ } 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)), +) - -@given( - coords=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]], @@ -65,9 +146,7 @@ def test_coordinates_str_roundtrip( assert coordinate.to_string(precision=10) == new_c.to_string(precision=10) -@given( - coords=line_coords(srs=epsg4326, min_points=1), -) +@coordinates() def test_coordinates_repr_roundtrip( coords: typing.Union[ typing.Sequence[typing.Tuple[float, float]], @@ -82,11 +161,7 @@ def test_coordinates_repr_roundtrip( assert coordinate == new_c -@given( - id=st.one_of(st.none(), st.text(alphabet=string.printable)), - target_id=st.one_of(st.none(), st.text(alphabet=string.printable)), - extrude=st.one_of(st.none(), st.booleans()), - altitude_mode=st.one_of(st.none(), st.sampled_from(AltitudeMode)), +@common_geometry( geometry=st.one_of( st.none(), points(srs=epsg4326), @@ -97,6 +172,7 @@ def test_point_repr_roundtrip( 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( @@ -107,16 +183,10 @@ def test_point_repr_roundtrip( geometry=geometry, ) - new_p = eval(repr(point), {}, eval_locals) # noqa: S307 - - assert point == new_p + _test_repr_roundtrip(point) -@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()), - altitude_mode=st.one_of(st.none(), st.sampled_from(AltitudeMode)), +@common_geometry( geometry=st.one_of( st.none(), points(srs=epsg4326), @@ -126,6 +196,7 @@ 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: @@ -137,17 +208,10 @@ def test_point_str_roundtrip( geometry=geometry, ) - new_p = fastkml.geometry.Point.class_from_string(point.to_string()) - - assert point.to_string() == new_p.to_string() - assert point.geometry == new_p.geometry + _test_geometry_str_roundtrip(point) -@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()), - altitude_mode=st.one_of(st.none(), st.sampled_from(AltitudeMode)), +@common_geometry( geometry=st.one_of( st.none(), points(srs=epsg4326), @@ -157,6 +221,7 @@ 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: @@ -168,25 +233,10 @@ def test_point_str_roundtrip_terse( geometry=geometry, ) - new_p = fastkml.geometry.Point.class_from_string( - point.to_string(precision=15, verbosity=Verbosity.terse), - ) - - assert (new_p.altitude_mode is None) or ( - new_p.altitude_mode != AltitudeMode.clamp_to_ground - ) - assert (new_p.extrude is None) or (new_p.extrude is True) - assert point.to_string(verbosity=Verbosity.verbose, precision=6) == new_p.to_string( - verbosity=Verbosity.verbose, - precision=6, - ) + _test_geometry_str_roundtrip_terse(point) -@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()), - altitude_mode=st.one_of(st.none(), st.sampled_from(AltitudeMode)), +@common_geometry( geometry=st.one_of( st.none(), points(srs=epsg4326), @@ -196,6 +246,7 @@ 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: @@ -207,24 +258,10 @@ def test_point_str_roundtrip_verbose( geometry=geometry, ) - new_p = fastkml.geometry.Point.class_from_string( - point.to_string(verbosity=Verbosity.verbose), - ) - - assert isinstance(new_p.altitude_mode, AltitudeMode) - assert isinstance(new_p.extrude, bool) - assert point.to_string(verbosity=Verbosity.terse, precision=16) == new_p.to_string( - verbosity=Verbosity.terse, - precision=16, - ) + _test_geometry_str_roundtrip_verbose(point) -@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)), +@common_geometry( geometry=st.one_of( st.none(), line_strings(srs=epsg4326), @@ -247,17 +284,10 @@ def test_linestring_repr_roundtrip( geometry=geometry, ) - new_l = eval(repr(line), {}, eval_locals) # noqa: S307 - - assert line == new_l + _test_repr_roundtrip(line) -@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)), +@common_geometry( geometry=st.one_of( st.none(), line_strings(srs=epsg4326), @@ -280,18 +310,10 @@ def test_linestring_str_roundtrip( geometry=geometry, ) - new_l = fastkml.geometry.LineString.class_from_string(line.to_string()) - - assert line.to_string() == new_l.to_string() - assert line == new_l + _test_geometry_str_roundtrip(line) -@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)), +@common_geometry( geometry=st.one_of( st.none(), line_strings(srs=epsg4326), @@ -314,21 +336,10 @@ def test_linestring_str_roundtrip_terse( geometry=geometry, ) - new_l = fastkml.geometry.LineString.class_from_string( - line.to_string(verbosity=Verbosity.terse), - ) + _test_geometry_str_roundtrip_terse(line) - assert line.to_string(verbosity=Verbosity.verbose) == new_l.to_string( - verbosity=Verbosity.verbose, - ) - -@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)), +@common_geometry( geometry=st.one_of( st.none(), line_strings(srs=epsg4326), @@ -351,21 +362,10 @@ def test_linestring_str_roundtrip_verbose( geometry=geometry, ) - new_l = fastkml.geometry.LineString.class_from_string( - line.to_string(verbosity=Verbosity.verbose), - ) - - assert line.to_string(verbosity=Verbosity.terse) == new_l.to_string( - verbosity=Verbosity.terse, - ) + _test_geometry_str_roundtrip_verbose(line) -@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)), +@common_geometry( geometry=st.one_of( st.none(), polygons(srs=epsg4326), @@ -388,17 +388,10 @@ def test_polygon_repr_roundtrip( geometry=geometry, ) - new_p = eval(repr(polygon), {}, eval_locals) # noqa: S307 + _test_repr_roundtrip(polygon) - assert polygon == new_p - -@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)), +@common_geometry( geometry=st.one_of( st.none(), polygons(srs=epsg4326), @@ -421,18 +414,10 @@ def test_polygon_str_roundtrip( geometry=geometry, ) - new_p = fastkml.geometry.Polygon.class_from_string(polygon.to_string()) - - assert polygon.to_string() == new_p.to_string() - assert polygon == new_p + _test_geometry_str_roundtrip(polygon) -@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)), +@common_geometry( geometry=st.one_of( st.none(), polygons(srs=epsg4326), @@ -455,21 +440,10 @@ def test_polygon_str_roundtrip_terse( geometry=geometry, ) - new_p = fastkml.geometry.Polygon.class_from_string( - polygon.to_string(verbosity=Verbosity.terse), - ) - - assert polygon.to_string(verbosity=Verbosity.verbose) == new_p.to_string( - verbosity=Verbosity.verbose, - ) + _test_geometry_str_roundtrip_terse(polygon) -@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)), +@common_geometry( geometry=st.one_of( st.none(), polygons(srs=epsg4326), @@ -492,10 +466,4 @@ def test_polygon_str_roundtrip_verbose( geometry=geometry, ) - new_p = fastkml.geometry.Polygon.class_from_string( - polygon.to_string(verbosity=Verbosity.verbose), - ) - - assert polygon.to_string(verbosity=Verbosity.verbose) == new_p.to_string( - verbosity=Verbosity.verbose, - ) + _test_geometry_str_roundtrip_verbose(polygon) diff --git a/tests/hypothesis/multi_geometry_test.py b/tests/hypothesis/multi_geometry_test.py index 19950372..d237a740 100644 --- a/tests/hypothesis/multi_geometry_test.py +++ b/tests/hypothesis/multi_geometry_test.py @@ -17,6 +17,7 @@ """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 @@ -53,13 +54,17 @@ ID_TEXT = string.ascii_letters + string.digits + string.punctuation - -@given( +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)), +) + + +@common_geometry( geometry=st.one_of( st.none(), multi_points(srs=epsg4326), @@ -89,12 +94,7 @@ def test_multipoint_repr_roundtrip( assert isinstance(new_mg.geometry, MultiPoint) -@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)), +@common_geometry( geometry=st.one_of( st.none(), multi_points(srs=epsg4326), @@ -127,12 +127,7 @@ def test_multipoint_str_roundtrip( assert isinstance(new_mg.geometry, MultiPoint) -@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)), +@common_geometry( geometry=st.one_of( st.none(), multi_points(srs=epsg4326), @@ -166,12 +161,7 @@ def test_multipoint_str_roundtrip_terse( assert isinstance(new_mg.geometry, MultiPoint) -@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)), +@common_geometry( geometry=st.one_of( st.none(), multi_points(srs=epsg4326), @@ -205,12 +195,7 @@ def test_multipoint_str_roundtrip_verbose( assert isinstance(new_mg.geometry, MultiPoint) -@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)), +@common_geometry( geometry=st.one_of( st.none(), multi_line_strings(srs=epsg4326), @@ -240,12 +225,7 @@ def test_multilinestring_repr_roundtrip( assert isinstance(new_mg.geometry, MultiLineString) -@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)), +@common_geometry( geometry=st.one_of( st.none(), multi_line_strings(srs=epsg4326), @@ -278,12 +258,7 @@ def test_multilinestring_str_roundtrip( assert isinstance(new_mg.geometry, MultiLineString) -@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)), +@common_geometry( geometry=st.one_of( st.none(), multi_line_strings(srs=epsg4326), @@ -317,12 +292,7 @@ def test_multilinestring_str_roundtrip_terse( assert isinstance(new_mg.geometry, MultiLineString) -@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)), +@common_geometry( geometry=st.one_of( st.none(), multi_line_strings(srs=epsg4326), @@ -356,12 +326,7 @@ def test_multilinestring_str_roundtrip_verbose( assert isinstance(new_mg.geometry, MultiLineString) -@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)), +@common_geometry( geometry=st.one_of( st.none(), multi_polygons(srs=epsg4326), @@ -391,12 +356,7 @@ def test_multipolygon_repr_roundtrip( assert isinstance(new_mg.geometry, MultiPolygon) -@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)), +@common_geometry( geometry=st.one_of( st.none(), multi_polygons(srs=epsg4326), @@ -429,12 +389,7 @@ def test_multipolygon_str_roundtrip( assert isinstance(new_mg.geometry, MultiPolygon) -@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)), +@common_geometry( geometry=st.one_of( st.none(), multi_polygons(srs=epsg4326), @@ -468,12 +423,7 @@ def test_multipolygon_str_roundtrip_terse( assert isinstance(new_mg.geometry, MultiPolygon) -@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)), +@common_geometry( geometry=st.one_of( st.none(), multi_polygons(srs=epsg4326), @@ -507,12 +457,7 @@ def test_multipolygon_str_roundtrip_verbose( assert isinstance(new_mg.geometry, MultiPolygon) -@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)), +@common_geometry( geometry=st.one_of( st.none(), geometry_collections(srs=epsg4326), @@ -547,12 +492,7 @@ def test_geometrycollection_repr_roundtrip( assert not new_mg -@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)), +@common_geometry( geometry=st.one_of( st.none(), geometry_collections(srs=epsg4326), @@ -588,12 +528,7 @@ def test_geometrycollection_str_roundtrip( assert not new_mg -@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)), +@common_geometry( geometry=st.one_of( st.none(), geometry_collections(srs=epsg4326), @@ -629,12 +564,7 @@ def test_geometrycollection_str_roundtrip_terse( assert not new_mg -@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)), +@common_geometry( geometry=st.one_of( st.none(), geometry_collections(srs=epsg4326), From b9eda46ab0f664cb3b6eb83b2979ec1e4508e238 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sun, 13 Oct 2024 13:57:32 +0100 Subject: [PATCH 13/15] Refactor hypothesis tests to avoid code duplication --- tests/hypothesis/multi_geometry_test.py | 263 ++++++++++++------------ 1 file changed, 127 insertions(+), 136 deletions(-) diff --git a/tests/hypothesis/multi_geometry_test.py b/tests/hypothesis/multi_geometry_test.py index d237a740..f8e5652a 100644 --- a/tests/hypothesis/multi_geometry_test.py +++ b/tests/hypothesis/multi_geometry_test.py @@ -15,8 +15,9 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA """Property based tests of the Geometry classes.""" +from __future__ import annotations + import string -import typing from functools import partial from hypothesis import given @@ -64,6 +65,29 @@ ) +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], +) -> 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 geometry: + assert type(new_g.geometry) is cls + + @common_geometry( geometry=st.one_of( st.none(), @@ -71,12 +95,12 @@ ), ) def test_multipoint_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[MultiPoint], + 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, @@ -87,11 +111,7 @@ def test_multipoint_repr_roundtrip( geometry=geometry, ) - new_mg = eval(repr(multi_geometry), {}, eval_locals) # noqa: S307 - - assert multi_geometry == new_mg - if geometry: - assert isinstance(new_mg.geometry, MultiPoint) + _test_repr_roundtrip(multi_geometry, MultiPoint) @common_geometry( @@ -101,12 +121,12 @@ def test_multipoint_repr_roundtrip( ), ) def test_multipoint_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[MultiPoint], + 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, @@ -117,14 +137,7 @@ def test_multipoint_str_roundtrip( geometry=geometry, ) - new_mg = fastkml.geometry.MultiGeometry.class_from_string( - multi_geometry.to_string(), - ) - - assert multi_geometry.to_string() == new_mg.to_string() - assert multi_geometry == new_mg - if geometry: - assert isinstance(new_mg.geometry, MultiPoint) + _test_geometry_str_roundtrip(multi_geometry, MultiPoint) @common_geometry( @@ -134,12 +147,12 @@ def test_multipoint_str_roundtrip( ), ) def test_multipoint_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[MultiPoint], + 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, @@ -168,12 +181,12 @@ def test_multipoint_str_roundtrip_terse( ), ) def test_multipoint_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[MultiPoint], + 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, @@ -202,12 +215,12 @@ def test_multipoint_str_roundtrip_verbose( ), ) def test_multilinestring_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[MultiLineString], + 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, @@ -218,11 +231,7 @@ def test_multilinestring_repr_roundtrip( geometry=geometry, ) - new_mg = eval(repr(multi_geometry), {}, eval_locals) # noqa: S307 - - assert multi_geometry == new_mg - if geometry: - assert isinstance(new_mg.geometry, MultiLineString) + _test_repr_roundtrip(multi_geometry, MultiLineString) @common_geometry( @@ -232,12 +241,12 @@ def test_multilinestring_repr_roundtrip( ), ) def test_multilinestring_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[MultiLineString], + 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, @@ -248,14 +257,7 @@ def test_multilinestring_str_roundtrip( geometry=geometry, ) - new_mg = fastkml.geometry.MultiGeometry.class_from_string( - multi_geometry.to_string(), - ) - - assert multi_geometry.to_string() == new_mg.to_string() - assert multi_geometry == new_mg - if geometry: - assert isinstance(new_mg.geometry, MultiLineString) + _test_geometry_str_roundtrip(multi_geometry, MultiLineString) @common_geometry( @@ -265,12 +267,12 @@ def test_multilinestring_str_roundtrip( ), ) def test_multilinestring_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[MultiLineString], + 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, @@ -299,12 +301,12 @@ def test_multilinestring_str_roundtrip_terse( ), ) def test_multilinestring_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[MultiLineString], + 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, @@ -333,12 +335,12 @@ def test_multilinestring_str_roundtrip_verbose( ), ) def test_multipolygon_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[MultiPolygon], + 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, @@ -349,11 +351,7 @@ def test_multipolygon_repr_roundtrip( geometry=geometry, ) - new_mg = eval(repr(multi_geometry), {}, eval_locals) # noqa: S307 - - assert multi_geometry == new_mg - if geometry: - assert isinstance(new_mg.geometry, MultiPolygon) + _test_repr_roundtrip(multi_geometry, MultiPolygon) @common_geometry( @@ -363,12 +361,12 @@ def test_multipolygon_repr_roundtrip( ), ) def test_multipolygon_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[MultiPolygon], + 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, @@ -379,14 +377,7 @@ def test_multipolygon_str_roundtrip( geometry=geometry, ) - new_mg = fastkml.geometry.MultiGeometry.class_from_string( - multi_geometry.to_string(), - ) - - assert multi_geometry.to_string() == new_mg.to_string() - assert multi_geometry == new_mg - if geometry: - assert isinstance(new_mg.geometry, MultiPolygon) + _test_geometry_str_roundtrip(multi_geometry, MultiPolygon) @common_geometry( @@ -396,12 +387,12 @@ def test_multipolygon_str_roundtrip( ), ) def test_multipolygon_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[MultiPolygon], + 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, @@ -430,12 +421,12 @@ def test_multipolygon_str_roundtrip_terse( ), ) def test_multipolygon_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[MultiPolygon], + 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, @@ -464,12 +455,12 @@ def test_multipolygon_str_roundtrip_verbose( ), ) def test_geometrycollection_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[GeometryCollection], + 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, @@ -499,12 +490,12 @@ def test_geometrycollection_repr_roundtrip( ), ) def test_geometrycollection_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[GeometryCollection], + 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, @@ -535,12 +526,12 @@ def test_geometrycollection_str_roundtrip( ), ) def test_geometrycollection_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[GeometryCollection], + 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, @@ -571,12 +562,12 @@ def test_geometrycollection_str_roundtrip_terse( ), ) def test_geometrycollection_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[GeometryCollection], + 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, From fd79e877dcdb43eb9f2294327352d1ee40508042 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sun, 13 Oct 2024 14:39:58 +0100 Subject: [PATCH 14/15] fix doc tests in usage guide --- docs/usage_guide.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 1142a1e54923d46309c049e016b4b6316d072379 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sun, 13 Oct 2024 17:35:43 +0100 Subject: [PATCH 15/15] Refactor hypothesis multi-geometry tests to avoid code duplication --- tests/hypothesis/multi_geometry_test.py | 196 +++++++++++++++++------- 1 file changed, 143 insertions(+), 53 deletions(-) diff --git a/tests/hypothesis/multi_geometry_test.py b/tests/hypothesis/multi_geometry_test.py index f8e5652a..144f8963 100644 --- a/tests/hypothesis/multi_geometry_test.py +++ b/tests/hypothesis/multi_geometry_test.py @@ -78,14 +78,98 @@ def _test_repr_roundtrip( 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 geometry: - assert type(new_g.geometry) is cls + 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( @@ -137,7 +221,13 @@ def test_multipoint_str_roundtrip( geometry=geometry, ) - _test_geometry_str_roundtrip(multi_geometry, MultiPoint) + _test_geometry_str_roundtrip( + multi_geometry, + cls=MultiPoint, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + ) @common_geometry( @@ -163,15 +253,13 @@ def test_multipoint_str_roundtrip_terse( geometry=geometry, ) - new_mg = fastkml.geometry.MultiGeometry.class_from_string( - multi_geometry.to_string(verbosity=Verbosity.terse), - ) - - assert multi_geometry.to_string(verbosity=Verbosity.verbose) == new_mg.to_string( - verbosity=Verbosity.verbose, + _test_geometry_str_roundtrip_terse( + multi_geometry, + cls=MultiPoint, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, ) - if geometry: - assert isinstance(new_mg.geometry, MultiPoint) @common_geometry( @@ -197,15 +285,13 @@ def test_multipoint_str_roundtrip_verbose( geometry=geometry, ) - new_mg = fastkml.geometry.MultiGeometry.class_from_string( - multi_geometry.to_string(verbosity=Verbosity.verbose), - ) - - assert multi_geometry.to_string(verbosity=Verbosity.verbose) == new_mg.to_string( - verbosity=Verbosity.verbose, + _test_geometry_str_roundtrip_verbose( + multi_geometry, + cls=MultiPoint, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, ) - if geometry: - assert isinstance(new_mg.geometry, MultiPoint) @common_geometry( @@ -257,7 +343,13 @@ def test_multilinestring_str_roundtrip( geometry=geometry, ) - _test_geometry_str_roundtrip(multi_geometry, MultiLineString) + _test_geometry_str_roundtrip( + multi_geometry, + cls=MultiLineString, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + ) @common_geometry( @@ -283,15 +375,13 @@ def test_multilinestring_str_roundtrip_terse( geometry=geometry, ) - new_mg = fastkml.geometry.MultiGeometry.class_from_string( - multi_geometry.to_string(verbosity=Verbosity.terse), - ) - - assert multi_geometry.to_string(verbosity=Verbosity.verbose) == new_mg.to_string( - verbosity=Verbosity.verbose, + _test_geometry_str_roundtrip_terse( + multi_geometry, + cls=MultiLineString, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, ) - if geometry: - assert isinstance(new_mg.geometry, MultiLineString) @common_geometry( @@ -317,15 +407,13 @@ def test_multilinestring_str_roundtrip_verbose( geometry=geometry, ) - new_mg = fastkml.geometry.MultiGeometry.class_from_string( - multi_geometry.to_string(verbosity=Verbosity.verbose), - ) - - assert multi_geometry.to_string(verbosity=Verbosity.verbose) == new_mg.to_string( - verbosity=Verbosity.verbose, + _test_geometry_str_roundtrip_verbose( + multi_geometry, + cls=MultiLineString, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, ) - if geometry: - assert isinstance(new_mg.geometry, MultiLineString) @common_geometry( @@ -377,7 +465,13 @@ def test_multipolygon_str_roundtrip( geometry=geometry, ) - _test_geometry_str_roundtrip(multi_geometry, MultiPolygon) + _test_geometry_str_roundtrip( + multi_geometry, + cls=MultiPolygon, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + ) @common_geometry( @@ -403,15 +497,13 @@ def test_multipolygon_str_roundtrip_terse( geometry=geometry, ) - new_mg = fastkml.geometry.MultiGeometry.class_from_string( - multi_geometry.to_string(verbosity=Verbosity.terse), - ) - - assert multi_geometry.to_string(verbosity=Verbosity.verbose) == new_mg.to_string( - verbosity=Verbosity.verbose, + _test_geometry_str_roundtrip_terse( + multi_geometry, + cls=MultiPolygon, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, ) - if geometry: - assert isinstance(new_mg.geometry, MultiPolygon) @common_geometry( @@ -437,15 +529,13 @@ def test_multipolygon_str_roundtrip_verbose( geometry=geometry, ) - new_mg = fastkml.geometry.MultiGeometry.class_from_string( - multi_geometry.to_string(verbosity=Verbosity.verbose), - ) - - assert multi_geometry.to_string(verbosity=Verbosity.verbose) == new_mg.to_string( - verbosity=Verbosity.verbose, + _test_geometry_str_roundtrip_verbose( + multi_geometry, + cls=MultiPolygon, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, ) - if geometry: - assert isinstance(new_mg.geometry, MultiPolygon) @common_geometry(