From 01b61444dbca7e95bc7ee50a0f622a66ed3ec32a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 23 Sep 2024 16:37:54 +0000 Subject: [PATCH 01/13] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.6.5 → v0.6.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.5...v0.6.7) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 83b8d57b..073cf74a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -50,7 +50,7 @@ repos: hooks: - id: isort - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.6.5' + rev: 'v0.6.7' hooks: - id: ruff - repo: https://github.com/PyCQA/flake8 From d04e56b8f6b1f6ca0986369579638e7059d9cc0c Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Wed, 25 Sep 2024 18:05:23 +0100 Subject: [PATCH 02/13] Refactor contributing documentation and add setup instructions --- docs/contributing.rst | 92 ++++++++++++++++++++----------------------- 1 file changed, 43 insertions(+), 49 deletions(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index 319aaa39..2ade1e87 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -11,76 +11,70 @@ especially in the following ways: * Commenting on open issues and pull requests * Suggesting new features +Setting Up Your Environment +--------------------------- -Pull Requests -------------- +Fork the repository and clone your fork to your local machine: -Start by submitting a pull request on GitHub against the ``develop`` branch of the -repository. Your pull request should provide a good description of the change -you are making, and/or the bug that you are fixing. +.. code-block:: bash + git clone https://github.com/yourusername/fastkml.git + cd fastkml + git checkout develop -Running Tests Locally ---------------------- +Next, set up a virtual environment. This helps to manage dependencies and avoid conflicts: -You can make use of tox_ >= 1.8 to test the entire matrix of options: +.. code-block:: bash -* with / without lxml -* py36,py37,py38,py39 + python3 -m venv .venv + source venv/bin/activate # On Windows use `venv\Scripts\activate` -as well as pep8 style checking in a single call (this approximates what happens -when the package is run through Travis-CI) +Then, install the required packages: -.. code-block:: python +.. code-block:: bash - # Install tox - pip install tox>=1.8 + pip install -e ".[dev]" - # Run tox - tox +Install the ``pre-commit`` hook with: - # Or optionally - # (to skip tests for Python versions you do not have installed) - tox --skip-missing-interpreters +.. code-block:: bash -This will run through all of the tests and produce an output similar to:: + pre-commit install - ______________________________________________________ summary ______________________________________________________ - SKIPPED: py36: InterpreterNotFound: python3.6 - py37: commands succeeded - py38: commands succeeded - py39: commands succeeded - SKIPPED: py36-lxml: InterpreterNotFound: python3.6 - py37-lxml: commands succeeded - py38-lxml: commands succeeded - py39-lxml: commands succeeded - pep8: commands succeeded - docs: commands succeeded - congratulations :) +and check the code with: -You are primarily looking for the ``congratulations :)`` line at the bottom, -signifying that the code is working as expected on all configurations -available. +.. code-block:: bash -.. _tox: https://pypi.python.org/pypi/tox + pre-commit run --all-files -coverage -~~~~~~~~ +Running the Tests +----------------- -You can also run the tests with coverage_ to see which lines are covered by the -tests. This is useful for writing new tests to cover any uncovered lines:: +To run the tests, simply use: - pytest tests --cov=fastkml --cov=tests --cov-report=xml +.. code-block:: bash + pytest -pre-commit -~~~~~~~~~~~ +You can also run the tests with `coverage `_ +to see which lines are covered by the tests. +This is useful for writing new tests to cover any uncovered lines: -Install the ``pre-commit`` hook with:: +.. code-block:: bash - pip install pre-commit - pre-commit install + pytest --cov=fastkml --cov-report=term -and check the code with:: +To get a report on the individual lines that are not covered, use the +``--cov-report=term-missing`` option, or generate an HTML report with +``--cov-report=html``. +Some editor extensions can also show the coverage directly in the editor, notably +`coverage-gutter `_ +for VSCode, which needs the output to be in the ``xml`` format produced with +``--cov-report=xml``. - pre-commit run --all-files + +Tips +---- + +- Commit often, commit early. +- Make a draft PR while you are still working on it to give your work some visibility. From 744cd50444134c7a0d7ed652eaf1c3e36a2c9be7 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Wed, 25 Sep 2024 18:48:41 +0100 Subject: [PATCH 03/13] improve type annotations for tests --- pyproject.toml | 3 ++- tests/base_test.py | 4 ++-- tests/geometries/geometry_test.py | 15 ++++++++++++++- tests/styles_test.py | 24 +++++++++++++++--------- 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e716ecae..d8a2cfbb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -147,7 +147,8 @@ ignore_missing_imports = true implicit_reexport = false no_implicit_optional = true overrides = [ - { disable_error_code = "attr-defined, union-attr", module = "tests.*" }, + { disable_error_code = "attr-defined, union-attr", module = "tests.oldunit_test" }, + { disable_error_code = "union-attr", module = "tests.*" }, ] show_error_codes = true strict_equality = true diff --git a/tests/base_test.py b/tests/base_test.py index a92481f8..5f1b742f 100644 --- a/tests/base_test.py +++ b/tests/base_test.py @@ -46,8 +46,8 @@ def test_custom_kwargs(self) -> None: altkw=2, ) - assert obj.custom == "custom" - assert obj.altkw == 2 + assert obj.custom == "custom" # type: ignore[attr-defined] + assert obj.altkw == 2 # type: ignore[attr-defined] def test_custom_kwargs_splat(self) -> None: obj = kml_base._BaseObject( diff --git a/tests/geometries/geometry_test.py b/tests/geometries/geometry_test.py index 0d47a3f5..223990fe 100644 --- a/tests/geometries/geometry_test.py +++ b/tests/geometries/geometry_test.py @@ -19,7 +19,7 @@ from pygeoif import geometry as geo from fastkml import exceptions -from fastkml.geometry import AltitudeMode +from fastkml.enums import AltitudeMode from fastkml.geometry import LinearRing from fastkml.geometry import LineString from fastkml.geometry import MultiGeometry @@ -421,6 +421,7 @@ def test_create_kml_geometry_point(self) -> None: """Test the create_kml_geometry function.""" g = create_kml_geometry(geo.Point(0, 1)) + assert isinstance(g, Point) assert g.geometry.__geo_interface__ == { "type": "Point", "bbox": (0.0, 1.0, 0.0, 1.0), @@ -433,6 +434,7 @@ def test_create_kml_geometry_linestring(self) -> None: """Test the create_kml_geometry function.""" g = create_kml_geometry(geo.LineString([(0, 0), (1, 1)])) + assert isinstance(g, LineString) assert g.geometry.__geo_interface__ == { "type": "LineString", "bbox": (0.0, 0.0, 1.0, 1.0), @@ -445,6 +447,7 @@ def test_create_kml_geometry_linearring(self) -> None: """Test the create_kml_geometry function.""" g = create_kml_geometry(geo.LinearRing([(0, 0), (1, 1), (1, 0), (0, 0)])) + assert isinstance(g, LinearRing) assert g.geometry.__geo_interface__ == { "type": "LinearRing", "bbox": (0.0, 0.0, 1.0, 1.0), @@ -460,6 +463,7 @@ def test_create_kml_geometry_polygon(self) -> None: """Test the create_kml_geometry function.""" g = create_kml_geometry(geo.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)])) + assert isinstance(g, Polygon) assert g.geometry.__geo_interface__ == { "type": "Polygon", "bbox": (0.0, 0.0, 1.0, 1.0), @@ -475,6 +479,8 @@ def test_create_kml_geometry_multipoint(self) -> None: """Test the create_kml_geometry function.""" g = create_kml_geometry(geo.MultiPoint([(0, 0), (1, 1), (1, 0), (2, 2)])) + assert isinstance(g, MultiGeometry) + assert g.geometry is not None assert len(g.geometry) == 4 assert "MultiGeometry>" in g.to_string() assert "Point>" in g.to_string() @@ -489,6 +495,8 @@ def test_create_kml_geometry_multilinestring(self) -> None: geo.MultiLineString([[(0, 0), (1, 1)], [(0, 0), (1, 1)]]), ) + assert isinstance(g, MultiGeometry) + assert g.geometry is not None assert len(g.geometry) == 2 assert "MultiGeometry>" in g.to_string() assert "LineString>" in g.to_string() @@ -508,6 +516,9 @@ def test_create_kml_geometry_multipolygon(self) -> None: ), ), ) + + assert isinstance(g, MultiGeometry) + assert g.geometry is not None assert len(g.geometry) == 2 assert "MultiGeometry>" in g.to_string() assert "Polygon>" in g.to_string() @@ -541,6 +552,8 @@ def test_create_kml_geometry_geometrycollection(self) -> None: g = create_kml_geometry(gc) + assert isinstance(g, MultiGeometry) + assert g.geometry is not None assert len(g.geometry) == 7 assert "MultiGeometry>" in g.to_string() assert "LineString>" in g.to_string() diff --git a/tests/styles_test.py b/tests/styles_test.py index 949b5bd4..144bdc97 100644 --- a/tests/styles_test.py +++ b/tests/styles_test.py @@ -17,6 +17,7 @@ """Test the styles classes.""" import pytest +from fastkml import links from fastkml import styles from fastkml.enums import ColorMode from fastkml.enums import DisplayMode @@ -58,7 +59,7 @@ def test_icon_style(self) -> None: color_mode=ColorMode("random"), scale=1.0, heading=0, - icon=styles.Icon(href="http://example.com/icon.png"), + icon=links.Icon(href="http://example.com/icon.png"), ) serialized = icons.to_string() @@ -84,7 +85,7 @@ def test_icon_style_with_hot_spot(self) -> None: color_mode=ColorMode.random, scale=5.0, heading=20.0, - icon=styles.Icon( + icon=links.Icon( ns="{http://www.opengis.net/kml/2.2}", id="icon-id", href="", @@ -318,7 +319,7 @@ def test_style(self) -> None: color_mode=ColorMode.random, scale=1.0, heading=0, - icon=styles.Icon(href="http://example.com/icon.png"), + icon=links.Icon(href="http://example.com/icon.png"), ) lines = styles.LineStyle( id="id-l0", @@ -358,13 +359,13 @@ def test_style(self) -> None: serialized = style.to_string() - assert '' + assert '' in serialized assert "" in serialized assert "" in serialized - assert "" + assert "" in serialized assert " None: assert style.id == "id-0" assert style.target_id == "target-0" + assert isinstance(style.styles[0], styles.BalloonStyle) assert style.styles[0].id == "id-b0" assert style.styles[0].target_id == "target-b0" assert style.styles[0].bg_color == "7fff0000" @@ -419,6 +421,7 @@ def test_style_read(self) -> None: assert style.styles[0].text == "Hello" assert style.styles[0].display_mode == DisplayMode.hide + assert isinstance(style.styles[1], styles.IconStyle) assert style.styles[1].id == "id-i0" assert style.styles[1].target_id == "target-i0" assert style.styles[1].color == "ff0000ff" @@ -427,18 +430,21 @@ def test_style_read(self) -> None: assert style.styles[1].heading == 0 assert style.styles[1].icon.href == "http://example.com/icon.png" + assert isinstance(style.styles[2], styles.LabelStyle) assert style.styles[2].id == "id-a0" assert style.styles[2].target_id == "target-a0" assert style.styles[2].color == "ff0000ff" assert style.styles[2].color_mode == ColorMode.random assert style.styles[2].scale == 1.0 + assert isinstance(style.styles[3], styles.LineStyle) assert style.styles[3].id == "id-l0" assert style.styles[3].target_id == "target-l0" assert style.styles[3].color == "ff0000ff" assert style.styles[3].color_mode == ColorMode.normal assert style.styles[3].width == 1.0 + assert isinstance(style.styles[4], styles.PolyStyle) assert style.styles[4].id == "id-p0" assert style.styles[4].target_id == "target-p0" assert style.styles[4].color == "ff0000ff" @@ -446,7 +452,7 @@ def test_style_read(self) -> None: assert style.styles[4].fill == 0 assert style.styles[4].outline == 1 - def test_stylemap(self) -> None: + def test_stylemap(self) -> None: # noqa: PLR0915 url = styles.StyleUrl(id="id-0", url="#style-0", target_id="target-0") icons = styles.IconStyle( id="id-i0", @@ -455,7 +461,7 @@ def test_stylemap(self) -> None: color_mode=ColorMode.random, scale=1.0, heading=0, - icon=styles.Icon(href="http://example.com/icon.png"), + icon=links.Icon(href="http://example.com/icon.png"), ) lines = styles.LineStyle( id="id-l0", From 9e8b3b76ac811e9fec3f600eb42315c93ee6971d Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Wed, 25 Sep 2024 18:55:43 +0100 Subject: [PATCH 04/13] run mypy for tests in CI --- .github/workflows/run-all-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index e49b889f..23462414 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -29,7 +29,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13-dev'] steps: - uses: actions/checkout@v4 @@ -90,7 +90,7 @@ jobs: pip install -e ".[typing, complexity, linting]" - name: Typecheck run: | - mypy fastkml + mypy fastkml tests - name: Linting run: | flake8 fastkml examples docs From 456f5cc8c37b886c6e2ee4fb4a2bf5c2327e3c64 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Wed, 25 Sep 2024 19:04:39 +0100 Subject: [PATCH 05/13] add pytest as dependency to typechecking --- .github/workflows/run-all-tests.yml | 2 +- tests/config_test.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index 23462414..d0ab05e2 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -87,7 +87,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip wheel - pip install -e ".[typing, complexity, linting]" + pip install -e ".[typing, complexity, linting, tests]" - name: Typecheck run: | mypy fastkml tests diff --git a/tests/config_test.py b/tests/config_test.py index c799e5a0..a7e722aa 100644 --- a/tests/config_test.py +++ b/tests/config_test.py @@ -16,7 +16,7 @@ """Test the configuration options.""" -import xml.etree.ElementTree +import xml.etree.ElementTree as ET import pytest @@ -31,7 +31,7 @@ def test_set_etree_implementation_xml() -> None: - config.set_etree_implementation(xml.etree.ElementTree) + config.set_etree_implementation(ET) assert config.etree.__name__ == "xml.etree.ElementTree" @@ -45,7 +45,7 @@ def test_set_etree_implementation_lxml() -> None: def test_register_namespaces() -> None: """Register namespaces for use in etree.""" - config.set_etree_implementation(xml.etree.ElementTree) + config.set_etree_implementation(ET) ns = { "real_person": "http://people.example.com", "role": "http://characters.example.com", @@ -67,7 +67,7 @@ def test_default_registered_namespaces() -> None: def test_set_default_namespaces() -> None: """Set the default namespaces.""" - config.set_etree_implementation(xml.etree.ElementTree) + config.set_etree_implementation(ET) config.etree._namespace_map = {} config.set_default_namespaces() From 27fb7a4331af9635718953ab48f4f62db1403391 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Wed, 25 Sep 2024 19:10:11 +0100 Subject: [PATCH 06/13] add python 3.14-dev to CI --- .github/workflows/run-all-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index d0ab05e2..5b2d46cb 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13-dev'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13-dev', '3.14-dev'] steps: - uses: actions/checkout@v4 From 031c3b3be1d31d580ca67cf70f86591da3bb4c41 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 16:37:44 +0000 Subject: [PATCH 07/13] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/abravalheri/validate-pyproject: v0.19 → v0.20.2](https://github.com/abravalheri/validate-pyproject/compare/v0.19...v0.20.2) - [github.com/astral-sh/ruff-pre-commit: v0.6.7 → v0.6.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.7...v0.6.8) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 073cf74a..e5ff9a1a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: hooks: - id: pyprojectsort - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.19 + rev: v0.20.2 hooks: - id: validate-pyproject - repo: https://github.com/ikamensh/flynt/ @@ -50,7 +50,7 @@ repos: hooks: - id: isort - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.6.7' + rev: 'v0.6.8' hooks: - id: ruff - repo: https://github.com/PyCQA/flake8 From 181e2fe45a35c62f374962e1fe737f7f75f7009c Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Tue, 1 Oct 2024 11:01:51 +0100 Subject: [PATCH 08/13] add typos --- .pre-commit-config.yaml | 4 ++++ README.rst | 4 ++-- _typos.toml | 6 ++++++ tests/atom_test.py | 2 +- 4 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 _typos.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e5ff9a1a..53344162 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -82,6 +82,10 @@ repos: rev: "1.18.0" hooks: - id: blacken-docs + - repo: https://github.com/crate-ci/typos + rev: v1.25.0 + hooks: + - id: typos # - repo: https://github.com/Lucas-C/pre-commit-hooks-markup # rev: v1.0.1 # hooks: diff --git a/README.rst b/README.rst index 700145ea..6db81743 100644 --- a/README.rst +++ b/README.rst @@ -117,9 +117,9 @@ Optional Limitations =========== -*Tesselate*, *Extrude* and *Altitude Mode* are assigned to a Geometry or +*Tessellate*, *Extrude* and *Altitude Mode* are assigned to a Geometry or Geometry collection (MultiGeometry). You cannot assign different values of -*Tesselate*, *Extrude* or *Altitude Mode* on parts of a MultiGeometry. +*Tessellate*, *Extrude* or *Altitude Mode* on parts of a MultiGeometry. Currently, the only major feature missing for the full Google Earth experience is the `gx extension diff --git a/_typos.toml b/_typos.toml new file mode 100644 index 00000000..a1cb930e --- /dev/null +++ b/_typos.toml @@ -0,0 +1,6 @@ +[default.extend-words] +lod = "lod" +Lod = "Lod" + +[files] +extend-exclude = ["tests/ogc_conformance/data/kml/*.kml"] diff --git a/tests/atom_test.py b/tests/atom_test.py index f5b905a3..92c2e968 100644 --- a/tests/atom_test.py +++ b/tests/atom_test.py @@ -155,7 +155,7 @@ def test_atom_contributor_no_name(self) -> None: assert a.name is None assert "atom:name" not in a.to_string() - def test_atom_contributor_rountrip(self) -> None: + def test_atom_contributor_roundtrip(self) -> None: a = atom.Contributor( ns="{http://www.w3.org/2005/Atom}", name="Nobody", From 9dc9f7446be8dd1a6a2f8f4987684ab97f6accb0 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Wed, 2 Oct 2024 13:21:52 +0100 Subject: [PATCH 09/13] Add test case for #355 --- docs/HISTORY.rst | 2 +- tests/kml_test.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.rst b/docs/HISTORY.rst index e5e3255c..4d742416 100644 --- a/docs/HISTORY.rst +++ b/docs/HISTORY.rst @@ -80,7 +80,7 @@ Changelog ----------------- - specify minor python versions tested with Travis CI -- add support for tesselation, altitudeMode and extrude to Geometries +- add support for tessellation, altitudeMode and extrude to Geometries - move implementation of geometry from kml.Placemark to geometry.Geometry - add support for heterogenous GeometryCollection - python 3 compatible diff --git a/tests/kml_test.py b/tests/kml_test.py index 42c06b28..e9f73188 100644 --- a/tests/kml_test.py +++ b/tests/kml_test.py @@ -20,6 +20,7 @@ import pygeoif as geo +from fastkml import containers from fastkml import features from fastkml import kml from fastkml.containers import Document @@ -58,6 +59,49 @@ def test_kml(self) -> None: k2 = kml.KML.class_from_string(k.to_string(), ns="") assert k.to_string() == k2.to_string() + def test_kml_with_document(self) -> None: + """Kml file with document/folder/placemark/polygon.""" + doc = ( + '' + '' + "test" + "" + "" + "" + "" + "-93.7426720226024,57.4519411370713 -93.6051809086549,49.4316261567984 " + "-80.8643376828499,49.5232868994301 -81.2309806533767,57.4519411370713 " + "-81.2309806533767,57.4519411370713 -93.7426720226024,57.4519411370713" + "" + "" + "" + "" + "-91.8663227028478,56.050879726904 -91.7704563496718,53.9897531336206 " + "-90.1407283456804,54.0856194867966 -90.0927951690924,56.002946550316 " + "-91.8663227028478,56.050879726904" + "" + "" + "" + "" + "-85.4912102166459,55.90708019714 -85.4912102166459,54.0376863102086 " + "-83.8135490360665,54.0856194867966 -83.9094153892425,55.90708019714 " + "-85.4912102166459,55.90708019714" + "" + "" + "" + "" + "" + ) + k = kml.KML.class_from_string(doc) + assert len(k.features) == 1 + assert isinstance(k.features[0], Document) + assert len(k.features[0].features) == 1 + assert isinstance(k.features[0].features[0], containers.Folder) + assert len(k.features[0].features[0].features) == 1 + assert isinstance(k.features[0].features[0].features[0], features.Placemark) + assert isinstance(k.features[0].features[0].features[0].geometry, geo.Polygon) + assert len(list(k.features[0].features[0].features[0].geometry.interiors)) == 2 + class TestParseKML(StdLibrary): def test_parse_kml(self) -> None: From 3f760bb16e44a3444a4cdd34e0207c3ca3a2371a Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Wed, 2 Oct 2024 17:29:12 +0100 Subject: [PATCH 10/13] Fix inner boundaries for polygon #355 --- fastkml/geometry.py | 108 ++++++++++++++-------------- tests/geometries/boundaries_test.py | 28 ++++---- tests/geometries/geometry_test.py | 6 +- tests/geometries/polygon_test.py | 2 +- tests/repr_eq_test.py | 35 +++++---- 5 files changed, 86 insertions(+), 93 deletions(-) diff --git a/fastkml/geometry.py b/fastkml/geometry.py index e0827b44..ec0c85f6 100644 --- a/fastkml/geometry.py +++ b/fastkml/geometry.py @@ -911,15 +911,15 @@ class InnerBoundaryIs(_XMLObject): """Represents the inner boundary of a polygon in KML.""" _default_nsid = config.KML - kml_geometries: List[LinearRing] + kml_geometry: LinearRing def __init__( self, *, ns: Optional[str] = None, name_spaces: Optional[Dict[str, str]] = None, - geometries: Optional[Iterable[geo.LinearRing]] = None, - kml_geometries: Optional[Iterable[LinearRing]] = None, + geometry: Optional[geo.LinearRing] = None, + kml_geometry: Optional[LinearRing] = None, **kwargs: Any, ) -> None: """ @@ -931,37 +931,31 @@ def __init__( The namespace for the KML element, by default None. name_spaces : Optional[Dict[str, str]], optional The namespace dictionary for the KML element, by default None. - geometries : Optional[Iterable[geo.LinearRing]], optional - The geometries to be converted to KML geometries, by default None. - kml_geometries : Optional[Iterable[LinearRing]], optional - The KML geometries, by default None. + geometry : Optional[geo.LinearRing], optional + The geometrY to be converted to a KML geometry, by default None. + kml_geometry : Optional[LinearRing], optional + The KML geometry, by default None. **kwargs : Any Additional keyword arguments. Raises ------ GeometryError - If both `geometries` and `kml_geometries` are provided. + If both `geometry` and `kml_geometry` are provided. Notes ----- - - If `geometries` is provided, it will be converted to KML geometries and - stored in `kml_geometries`. - - If `geometries` is not provided, `kml_geometries` will be an empty list. + - If `geometry` is provided, it will be converted to KML geometries and + stored in `kml_geometry`. + - If `geometry` and `kml_geometry` are both provided, a GeometryError will be + raised. """ - if geometries is not None and kml_geometries is not None: + if geometry is not None and kml_geometry is not None: raise GeometryError(MsgMutualExclusive) - if kml_geometries is None: - kml_geometries = ( - [ - LinearRing(ns=ns, name_spaces=name_spaces, geometry=lr) - for lr in geometries - ] - if geometries - else None - ) - self.kml_geometries = list(kml_geometries) if kml_geometries else [] + if kml_geometry is None: + kml_geometry = LinearRing(ns=ns, name_spaces=name_spaces, geometry=geometry) + self.kml_geometry = kml_geometry super().__init__( ns=ns, name_spaces=name_spaces, @@ -970,7 +964,7 @@ def __init__( def __bool__(self) -> bool: """Return True if any of the inner boundary geometries exist.""" - return any(b.geometry for b in self.kml_geometries) + return bool(self.kml_geometry) def __repr__(self) -> str: """Create a string (c)representation for InnerBoundaryIs.""" @@ -978,7 +972,7 @@ def __repr__(self) -> str: f"{self.__class__.__module__}.{self.__class__.__name__}(" f"ns={self.ns!r}, " f"name_spaces={self.name_spaces!r}, " - f"kml_geometries={self.kml_geometries!r}, " + f"kml_geometry={self.kml_geometry!r}, " f"**{self._get_splat()}," ")" ) @@ -989,15 +983,13 @@ def get_tag_name(cls) -> str: return "innerBoundaryIs" @property - def geometries(self) -> Optional[Iterable[geo.LinearRing]]: + def geometry(self) -> Optional[geo.LinearRing]: """ Return the list of LinearRing objects representing the inner boundary. If no inner boundary geometries exist, returns None. """ - if not self.kml_geometries: - return None - return [lr.geometry for lr in self.kml_geometries if lr.geometry] + return self.kml_geometry.geometry if self.kml_geometry else None registry.register( @@ -1005,10 +997,10 @@ def geometries(self) -> Optional[Iterable[geo.LinearRing]]: item=RegistryItem( ns_ids=("kml",), classes=(LinearRing,), - attr_name="kml_geometries", + attr_name="kml_geometry", node_name="LinearRing", - get_kwarg=xml_subelement_list_kwarg, - set_element=xml_subelement_list, + get_kwarg=xml_subelement_kwarg, + set_element=xml_subelement, ), ) @@ -1042,8 +1034,8 @@ class Polygon(_Geometry): https://developers.google.com/kml/documentation/kmlreference#polygon """ - outer_boundary_is: Optional[OuterBoundaryIs] - inner_boundary_is: Optional[InnerBoundaryIs] + outer_boundary: Optional[OuterBoundaryIs] + inner_boundaries: Optional[List[InnerBoundaryIs]] def __init__( self, @@ -1055,8 +1047,8 @@ def __init__( extrude: Optional[bool] = None, tessellate: Optional[bool] = None, altitude_mode: Optional[AltitudeMode] = None, - outer_boundary_is: Optional[OuterBoundaryIs] = None, - inner_boundary_is: Optional[InnerBoundaryIs] = None, + outer_boundary: Optional[OuterBoundaryIs] = None, + inner_boundaries: Optional[Iterable[InnerBoundaryIs]] = None, geometry: Optional[geo.Polygon] = None, **kwargs: Any, ) -> None: @@ -1079,9 +1071,9 @@ def __init__( The tessellate flag of the element. altitude_mode : Optional[AltitudeMode] The altitude mode of the element. - outer_boundary_is : Optional[OuterBoundaryIs] + outer_boundary : Optional[OuterBoundaryIs] The outer boundary of the element. - inner_boundary_is : Optional[InnerBoundaryIs] + inner_boundaries : Optional[Iterable[InnerBoundaryIs]] The inner boundaries of the element. geometry : Optional[geo.Polygon] The geometry object of the element. @@ -1098,13 +1090,15 @@ def __init__( None """ - if outer_boundary_is is not None and geometry is not None: + if outer_boundary is not None and geometry is not None: raise GeometryError(MsgMutualExclusive) if geometry is not None: - outer_boundary_is = OuterBoundaryIs(geometry=geometry.exterior) - inner_boundary_is = InnerBoundaryIs(geometries=geometry.interiors) - self.outer_boundary_is = outer_boundary_is - self.inner_boundary_is = inner_boundary_is + outer_boundary = OuterBoundaryIs(geometry=geometry.exterior) + inner_boundaries = [ + InnerBoundaryIs(geometry=interior) for interior in geometry.interiors + ] + self.outer_boundary = outer_boundary + self.inner_boundaries = list(inner_boundaries) if inner_boundaries else [] super().__init__( ns=ns, name_spaces=name_spaces, @@ -1126,7 +1120,7 @@ def __bool__(self) -> bool: True if the outer boundary is defined, False otherwise. """ - return bool(self.outer_boundary_is) + return bool(self.outer_boundary) @property def geometry(self) -> Optional[geo.Polygon]: @@ -1139,15 +1133,19 @@ def geometry(self) -> Optional[geo.Polygon]: The geometry object representing the geometry of the Polygon. """ - if not self.outer_boundary_is: + if not self.outer_boundary: return None - if not self.inner_boundary_is: + if not self.inner_boundaries: return geo.Polygon.from_linear_rings( - cast(geo.LinearRing, self.outer_boundary_is.geometry), + cast(geo.LinearRing, self.outer_boundary.geometry), ) - return geo.Polygon.from_linear_rings( # type: ignore[misc] - cast(geo.LinearRing, self.outer_boundary_is.geometry), - *self.inner_boundary_is.geometries, + return geo.Polygon.from_linear_rings( + cast(geo.LinearRing, self.outer_boundary.geometry), + *[ + interior.geometry + for interior in self.inner_boundaries + if interior.geometry is not None + ], ) def __repr__(self) -> str: @@ -1169,8 +1167,8 @@ def __repr__(self) -> str: f"extrude={self.extrude!r}, " f"tessellate={self.tessellate!r}, " f"altitude_mode={self.altitude_mode}, " - f"outer_boundary_is={self.outer_boundary_is!r}, " - f"inner_boundary_is={self.inner_boundary_is!r}, " + f"outer_boundary={self.outer_boundary!r}, " + f"inner_boundaries={self.inner_boundaries!r}, " f"**{self._get_splat()!r}," ")" ) @@ -1181,7 +1179,7 @@ def __repr__(self) -> str: item=RegistryItem( ns_ids=("kml",), classes=(OuterBoundaryIs,), - attr_name="outer_boundary_is", + attr_name="outer_boundary", node_name="outerBoundaryIs", get_kwarg=xml_subelement_kwarg, set_element=xml_subelement, @@ -1192,10 +1190,10 @@ def __repr__(self) -> str: item=RegistryItem( ns_ids=("kml",), classes=(InnerBoundaryIs,), - attr_name="inner_boundary_is", + attr_name="inner_boundaries", node_name="innerBoundaryIs", - get_kwarg=xml_subelement_kwarg, - set_element=xml_subelement, + get_kwarg=xml_subelement_list_kwarg, + set_element=xml_subelement_list, ), ) diff --git a/tests/geometries/boundaries_test.py b/tests/geometries/boundaries_test.py index 2736d99d..adf27b43 100644 --- a/tests/geometries/boundaries_test.py +++ b/tests/geometries/boundaries_test.py @@ -60,10 +60,10 @@ def test_inner_boundary(self) -> None: """Test the init method.""" coords = ((1, 2), (2, 0), (0, 0), (1, 2)) inner_boundary = InnerBoundaryIs( - kml_geometries=[LinearRing(kml_coordinates=Coordinates(coords=coords))], + kml_geometry=LinearRing(kml_coordinates=Coordinates(coords=coords)), ) - assert inner_boundary.geometries == [geo.LinearRing(coords)] + assert inner_boundary.geometry == geo.LinearRing(coords) assert inner_boundary.to_string(prettyprint=False).strip() == ( '' "" @@ -71,8 +71,13 @@ def test_inner_boundary(self) -> None: "" ) - def test_read_inner_boundary(self) -> None: - """Test the from_string method.""" + def test_read_inner_boundary_multiple_linestrings(self) -> None: + """ + Test the from_string method. + + When there are multiple LinearRings in the innerBoundaryIs element + only the first one is used. + """ inner_boundary = InnerBoundaryIs.class_from_string( '' "" @@ -85,18 +90,9 @@ def test_read_inner_boundary(self) -> None: "", ) - assert inner_boundary.geometries == [ - geo.LinearRing(((1, 4), (2, 0), (0, 0), (1, 4))), - geo.LinearRing( - ( - (-122.366212, 37.818977, 30), - (-122.365424, 37.819294, 30), - (-122.365704, 37.819731, 30), - (-122.366488, 37.819402, 30), - (-122.366212, 37.818977, 30), - ), - ), - ] + assert inner_boundary.geometry == geo.LinearRing( + ((1, 4), (2, 0), (0, 0), (1, 4)), + ) class TestBoundariesLxml(Lxml, TestBoundaries): diff --git a/tests/geometries/geometry_test.py b/tests/geometries/geometry_test.py index 223990fe..7e96699f 100644 --- a/tests/geometries/geometry_test.py +++ b/tests/geometries/geometry_test.py @@ -64,7 +64,7 @@ def test_extrude(self) -> None: assert g.extrude is True - def test_tesselate(self) -> None: + def test_tessellate(self) -> None: doc = """ 0.000000,1.000000 1 @@ -224,7 +224,8 @@ def test_multipolygon(self) -> None: g = MultiGeometry.class_from_string(doc) - assert len(g.geometry) == 2 # type: ignore[arg-type] + assert g.geometry is not None + assert len(g.geometry) == 2 def test_geometrycollection(self) -> None: doc = """ @@ -464,6 +465,7 @@ def test_create_kml_geometry_polygon(self) -> None: g = create_kml_geometry(geo.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)])) assert isinstance(g, Polygon) + assert g.geometry is not None assert g.geometry.__geo_interface__ == { "type": "Polygon", "bbox": (0.0, 0.0, 1.0, 1.0), diff --git a/tests/geometries/polygon_test.py b/tests/geometries/polygon_test.py index cecb1cdc..935aa86d 100644 --- a/tests/geometries/polygon_test.py +++ b/tests/geometries/polygon_test.py @@ -179,7 +179,7 @@ def test_empty_polygon(self) -> None: polygon = Polygon.class_from_string(doc) assert not polygon.geometry - assert polygon.outer_boundary_is is not None + assert polygon.outer_boundary is not None assert "tessellate>1 Date: Wed, 2 Oct 2024 17:42:06 +0100 Subject: [PATCH 11/13] Apply suggestions from code review Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- fastkml/geometry.py | 4 ++-- tests/geometries/boundaries_test.py | 10 +++++++++- tests/geometries/polygon_test.py | 2 ++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/fastkml/geometry.py b/fastkml/geometry.py index ec0c85f6..343df712 100644 --- a/fastkml/geometry.py +++ b/fastkml/geometry.py @@ -911,7 +911,7 @@ class InnerBoundaryIs(_XMLObject): """Represents the inner boundary of a polygon in KML.""" _default_nsid = config.KML - kml_geometry: LinearRing + kml_geometry: Optional[LinearRing] def __init__( self, @@ -932,7 +932,7 @@ def __init__( name_spaces : Optional[Dict[str, str]], optional The namespace dictionary for the KML element, by default None. geometry : Optional[geo.LinearRing], optional - The geometrY to be converted to a KML geometry, by default None. + The geometry to be converted to a KML geometry, by default None. kml_geometry : Optional[LinearRing], optional The KML geometry, by default None. **kwargs : Any diff --git a/tests/geometries/boundaries_test.py b/tests/geometries/boundaries_test.py index adf27b43..181bb066 100644 --- a/tests/geometries/boundaries_test.py +++ b/tests/geometries/boundaries_test.py @@ -63,7 +63,15 @@ def test_inner_boundary(self) -> None: kml_geometry=LinearRing(kml_coordinates=Coordinates(coords=coords)), ) - assert inner_boundary.geometry == geo.LinearRing(coords) +def test_inner_boundary(self) -> None: + """Test the init method and __bool__.""" + coords = ((1, 2), (2, 0), (0, 0), (1, 2)) + inner_boundary = InnerBoundaryIs( + kml_geometry=LinearRing(kml_coordinates=Coordinates(coords=coords)), + ) + + assert inner_boundary.geometry == geo.LinearRing(coords) + assert bool(inner_boundary) is True assert inner_boundary.to_string(prettyprint=False).strip() == ( '' "" diff --git a/tests/geometries/polygon_test.py b/tests/geometries/polygon_test.py index 935aa86d..7cb8dae2 100644 --- a/tests/geometries/polygon_test.py +++ b/tests/geometries/polygon_test.py @@ -180,6 +180,8 @@ def test_empty_polygon(self) -> None: assert not polygon.geometry assert polygon.outer_boundary is not None + assert isinstance(polygon.outer_boundary, LinearRing) + assert len(polygon.inner_boundaries) == 0 assert "tessellate>1 Date: Wed, 2 Oct 2024 17:51:14 +0100 Subject: [PATCH 12/13] fix tests --- fastkml/geometry.py | 2 +- tests/geometries/boundaries_test.py | 12 +++--------- tests/geometries/polygon_test.py | 3 ++- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/fastkml/geometry.py b/fastkml/geometry.py index 343df712..6b314140 100644 --- a/fastkml/geometry.py +++ b/fastkml/geometry.py @@ -1035,7 +1035,7 @@ class Polygon(_Geometry): """ outer_boundary: Optional[OuterBoundaryIs] - inner_boundaries: Optional[List[InnerBoundaryIs]] + inner_boundaries: List[InnerBoundaryIs] def __init__( self, diff --git a/tests/geometries/boundaries_test.py b/tests/geometries/boundaries_test.py index 181bb066..4b413f08 100644 --- a/tests/geometries/boundaries_test.py +++ b/tests/geometries/boundaries_test.py @@ -59,19 +59,13 @@ def test_read_outer_boundary(self) -> None: def test_inner_boundary(self) -> None: """Test the init method.""" coords = ((1, 2), (2, 0), (0, 0), (1, 2)) + inner_boundary = InnerBoundaryIs( kml_geometry=LinearRing(kml_coordinates=Coordinates(coords=coords)), ) -def test_inner_boundary(self) -> None: - """Test the init method and __bool__.""" - coords = ((1, 2), (2, 0), (0, 0), (1, 2)) - inner_boundary = InnerBoundaryIs( - kml_geometry=LinearRing(kml_coordinates=Coordinates(coords=coords)), - ) - - assert inner_boundary.geometry == geo.LinearRing(coords) - assert bool(inner_boundary) is True + assert inner_boundary.geometry == geo.LinearRing(coords) + assert bool(inner_boundary) assert inner_boundary.to_string(prettyprint=False).strip() == ( '' "" diff --git a/tests/geometries/polygon_test.py b/tests/geometries/polygon_test.py index 7cb8dae2..13763494 100644 --- a/tests/geometries/polygon_test.py +++ b/tests/geometries/polygon_test.py @@ -18,6 +18,7 @@ import pygeoif.geometry as geo +from fastkml.geometry import OuterBoundaryIs from fastkml.geometry import Polygon from tests.base import Lxml from tests.base import StdLibrary @@ -180,7 +181,7 @@ def test_empty_polygon(self) -> None: assert not polygon.geometry assert polygon.outer_boundary is not None - assert isinstance(polygon.outer_boundary, LinearRing) + assert isinstance(polygon.outer_boundary, OuterBoundaryIs) assert len(polygon.inner_boundaries) == 0 assert "tessellate>1 Date: Fri, 4 Oct 2024 15:04:20 +0100 Subject: [PATCH 13/13] back to dev 1.0.a14 --- README.rst | 4 ---- fastkml/about.py | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 6db81743..b60e4c9c 100644 --- a/README.rst +++ b/README.rst @@ -117,10 +117,6 @@ Optional Limitations =========== -*Tessellate*, *Extrude* and *Altitude Mode* are assigned to a Geometry or -Geometry collection (MultiGeometry). You cannot assign different values of -*Tessellate*, *Extrude* or *Altitude Mode* on parts of a MultiGeometry. - Currently, the only major feature missing for the full Google Earth experience is the `gx extension `_. diff --git a/fastkml/about.py b/fastkml/about.py index ee30bf21..d2b5364c 100644 --- a/fastkml/about.py +++ b/fastkml/about.py @@ -18,4 +18,4 @@ The only purpose of this module is to provide a version number for the package. """ -__version__ = "1.0.a13" +__version__ = "1.0.a14"