From 68b266af361f413a8a43cda92e2d7d52839b0e06 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 16:34:04 +0000 Subject: [PATCH 01/63] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - https://github.com/charliermarsh/ruff-pre-commit → https://github.com/astral-sh/ruff-pre-commit --- .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 b6edf3c7..e5cacfb8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -59,7 +59,7 @@ repos: rev: 5.12.0 hooks: - id: isort - - repo: https://github.com/charliermarsh/ruff-pre-commit + - repo: https://github.com/astral-sh/ruff-pre-commit rev: 'v0.0.292' hooks: - id: ruff From b3916eeed45a60fba807d1264758b06c709258dc Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Mon, 16 Oct 2023 18:51:07 +0100 Subject: [PATCH 02/63] fix #84 --- fastkml/geometry.py | 2 ++ setup.py | 2 +- tests/geometries/polygon_test.py | 12 ++++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/fastkml/geometry.py b/fastkml/geometry.py index 7a8aa5db..48575d44 100644 --- a/fastkml/geometry.py +++ b/fastkml/geometry.py @@ -131,6 +131,8 @@ def _etree_coordinates( Element, config.etree.Element(f"{self.ns}coordinates"), # type: ignore[attr-defined] ) + if not coordinates: + return element if len(coordinates[0]) == 2: tuples = (f"{c[0]:f},{c[1]:f}" for c in coordinates) elif len(coordinates[0]) == 3: diff --git a/setup.py b/setup.py index ca715166..175b8db7 100644 --- a/setup.py +++ b/setup.py @@ -62,7 +62,7 @@ def run_tests(self) -> None: "pygeoif>=1.1.0", "python-dateutil", "setuptools", - "typing-extensions", + "typing-extensions>4", ], entry_points=""" # -*- Entry points: -*- diff --git a/tests/geometries/polygon_test.py b/tests/geometries/polygon_test.py index 5274d233..6b791377 100644 --- a/tests/geometries/polygon_test.py +++ b/tests/geometries/polygon_test.py @@ -101,6 +101,18 @@ def test_from_string_exterior_interior(self): [[(0, 0), (1, 0), (1, 1), (0, 0)]], ) + def test_empty_polygon(self): + """Test empty polygon.""" + doc = ( + "1" + "" + ) + + polygon = cast(Polygon, Polygon.class_from_string(doc, ns="")) + + assert not polygon.geometry + assert polygon.to_string() + class TestLxml(Lxml, TestStdLibrary): """Test with lxml.""" From 463e5e7f545b05ad9e0633ca1854ab9bbe235723 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 16:36:01 +0000 Subject: [PATCH 03/63] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.9.1 → 23.10.0](https://github.com/psf/black/compare/23.9.1...23.10.0) - [github.com/astral-sh/ruff-pre-commit: v0.0.292 → v0.1.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.292...v0.1.1) --- .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 e5cacfb8..0df31dad 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,7 @@ repos: - id: pyupgrade args: ["--py3-plus", "--py37-plus"] - repo: https://github.com/psf/black - rev: 23.9.1 + rev: 23.10.0 hooks: - id: black - repo: https://github.com/PyCQA/flake8 @@ -60,7 +60,7 @@ repos: hooks: - id: isort - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.0.292' + rev: 'v0.1.1' hooks: - id: ruff - repo: https://github.com/PyCQA/flake8 From b7163cc0b30606bf4dffd5890b77b97944782ca7 Mon Sep 17 00:00:00 2001 From: Alex Svetkin Date: Thu, 26 Oct 2023 23:15:02 +0200 Subject: [PATCH 04/63] migrated setup.py into pyproject.toml #251 --- pyproject.toml | 102 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 85 insertions(+), 17 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a7216ab1..da502848 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,18 +1,77 @@ -[tool.ruff.extend-per-file-ignores] -"setup.py" = [ - "E501", +[build-system] +build-backend = "setuptools.build_meta" +requires = [ + "setuptools>=61.2", ] -"tests/oldunit_test.py" = [ - "E501", + +[project] +authors = [ + { email = "christian.ledermann@gmail.com", name = "Christian Ledermann" }, +] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Scientific/Engineering :: GIS", ] +dependencies = [ + "pygeoif>=1.1.0", + "python-dateutil", + "setuptools", + "typing-extensions>4", +] +description = "Fast KML processing in python" +keywords = [ + "GIS", + "Google", + "KML", + "Maps", + "OpenLayers", +] +name = "fastkml" +requires-python = ">=3.7" +version = "1.0.alpha.6" -[tool.isort] -force_single_line = true -line_length = 88 +[project.license] +text = "LGPL" + +[project.optional-dependencies] +testing = [ + "pytest", +] + +[project.readme] +content-type = "text/x-rst" +text = "Introduction\n============\nKML is an XML geospatial data format and an `OGC standard `_\nthat deserves a canonical python implementation.\nFastkml is a library to read, write and manipulate KML files. It aims to keep\nit simple and fast (using lxml_ if available). Fast refers to the time you\nspend to write and read KML files as well as the time you spend to get\nacquainted to the library or to create KML objects. It aims to provide all of\nthe functionality that KML clients such as `OpenLayers\n`_, `Google Maps `_, and\n`Google Earth `_ provides.\nGeometries are handled as pygeoif_ objects.\n.. _pygeoif: http://pypi.python.org/pypi/pygeoif/\n.. _lxml: https://pypi.python.org/pypi/lxml\n.. _dateutils: https://pypi.python.org/pypi/dateutils\n.. _pip: https://pypi.python.org/pypi/pip\nFastkml is continually tested\n.. image:: https://github.com/cleder/fastkml/actions/workflows/run-all-tests.yml/badge.svg?branch=main\n:target: https://github.com/cleder/fastkml/actions/workflows/run-all-tests.yml\n:alt: Test\n.. image:: http://codecov.io/github/cleder/fastkml/coverage.svg?branch=main\n:target: http://codecov.io/github/cleder/fastkml?branch=main\n:alt: codecov.io\n.. image:: https://img.shields.io/badge/code+style-black-000000.svg\n:target: https://github.com/psf/black\n:alt: Black\nIs Maintained and documented:\n.. image:: https://img.shields.io/pypi/v/fastkml.svg\n:target: https://pypi.python.org/pypi/fastkml\n:alt: Latest PyPI version\n.. image:: https://img.shields.io/pypi/status/fastkml.svg\n:target: https://pypi.python.org/pypi/fastkml/\n:alt: Development Status\n.. image:: https://readthedocs.org/projects/fastkml/badge/\n:target: https://fastkml.readthedocs.org/\n:alt: Documentation\n.. image:: https://www.openhub.net/p/fastkml/widgets/project_thin_badge.gif\n:target: https://www.openhub.net/p/fastkml\n:alt: Statistics from OpenHub\n.. image:: https://img.shields.io/pypi/pyversions/fastkml.svg\n:target: https://pypi.python.org/pypi/fastkml/\n:alt: Supported Python versions\n.. image:: https://img.shields.io/pypi/implementation/fastkml.svg\n:target: https://pypi.python.org/pypi/fastkml/\n:alt: Supported Python implementations\nDocumentation\n=============\nYou can find all of the documentation for FastKML at `fastkml.readthedocs.org\n`_. If you find something that is missing,\nplease submit a pull request on `GitHub `_\nwith the improvement.\nHave a look at Aryan Guptas\n`The Definite Guide to FastKML. `_\nAlternatives\n============\n`Keytree `_ provides a less comprehensive, but more flexible\napproach.\nInstall\n========\nYou can install the package with ``pip install fastkml`` which will pull in all requirements.\nRequirements\n-------------\n* pygeoif_\n* dateutils_\nOptional\n---------\n* lxml_\nYou can install all of the requirements for working with FastKML by using\npip_::\npip install -r requirements.txt\nLimitations\n===========\n*Tesselate*, *Extrude* and *Altitude Mode* are assigned to a Geometry or\nGeometry collection (MultiGeometry). You cannot assign different values of\n*Tesselate*, *Extrude* or *Altitude Mode* on parts of a MultiGeometry.\nCurrently, the only major feature missing for the full Google Earth experience\nis the `gx extension\n`_.\nPlease submit a PR with the features you'd like to see implemented.\nChangelog\n=========\n1.0 (unreleased)\n-----------------\n- Drop Python 2 support\n- Use pygeoif >=1.0\n- Drop shapely native support\n- Add type annotations\n- refactor\n0.12 (2020/09/23)\n-----------------\n- add track and multi track [dericke]\n- remove travis, add github actions\n- raise error and debug log for no geometries found case [hyperknot]\n- protect AttributeError from data element without value [fpassaniti]\n- examples and fixes [heltonbiker]\n- improve documentation [whatnick]\n0.11.1 (2015/07/13)\n-------------------\n- add travis deploy to travis.yml\n0.11 (2015/07/10)\n-----------------\n- handle coordinates tuples which contain spaces\n0.10 (2015/06/09)\n-----------------\n- Fix bug when the fill or outline attributes of a PolyStyle are float strings\n0.9 (2014/10/17)\n-----------------\n- Add tox.ini for running tests using tox [Ian Lee]\n- Add documentation, hosted at https://fastkml.readthedocs.org [Ian Lee]\n0.8 (2014/09/18)\n-----------------\n- Add support for address and phoneNumber [Ian Lee]\n- Add support for Ground Overlay kml [Ian Lee]\n0.7 (2014/08/01)\n----------------\n- Handle case where Document booleans (visibility,isopen) are 'true' or 'false' [jwhelland]\n- test case additions and lxml warning [Ian Lee]\n- pep8-ify source code (except test_main.py) [Ian Lee]\n- pyflakes-ify source code (except __init__.py) [Ian Lee]\n0.6 (2014/05/29)\n----------------\n- add Schema\n- add SchemaData\n- make use of lxmls default namespace\n0.5 (2013/10/23)\n-----------------\n- handle big files with huge_tree for lxml [Egil Moeller]\n- bugfixes\n0.4 (2013/09/05)\n-----------------\n- adds the ability to add untyped extended data / named value pairs [Denis Krienbuehl]\n0.3 (2012/11/15)\n-----------------\n- specify minor python versions tested with Travis CI\n- add support for tesselation, altitudeMode and extrude to Geometries\n- move implementation of geometry from kml.Placemark to geometry.Geometry\n- add support for heterogenous GeometryCollection\n- python 3 compatible\n- fix test for python 3\n- change license to LGPL\n- register namespaces for a more pleasant, human readable xml output\n0.2 (2012/07/27)\n-----------------\n- remove dependency on shapely\n- add more functionality\n0.1.1 (2012/06/29)\n------------------\n- add MANIFEST.in\n0.1 (2012/06/27)\n----------------\n- initial release" + +[project.urls] +Homepage = "https://github.com/cleder/fastkml" + +[tool.check-manifest] +ignore = [ + ".*", + "examples/*", + "mutmut_config.py", + "test-requirements.txt", + "tox.ini", +] [tool.flake8] max_line_length = 89 +[tool.isort] +force_single_line = true +line_length = 88 + [tool.mypy] disallow_any_generics = true disallow_incomplete_defs = true @@ -45,15 +104,6 @@ module = [ "fastkml.views", ] -[tool.check-manifest] -ignore = [ - ".*", - "examples/*", - "mutmut_config.py", - "test-requirements.txt", - "tox.ini", -] - [tool.pyright] exclude = [ "**/__pycache__", @@ -64,3 +114,21 @@ exclude = [ include = [ "fastkml", ] + +[tool.ruff.extend-per-file-ignores] +"setup.py" = [ + "E501", +] +"tests/oldunit_test.py" = [ + "E501", +] + +[tool.setuptools] +include-package-data = true +zip-safe = false + +[tool.setuptools.packages.find] +exclude = [ + "ez_setup", +] +namespaces = false From c46f0665dfe591ad297d2627579c9f02f01b2d8c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 26 Oct 2023 21:22:36 +0000 Subject: [PATCH 05/63] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pyproject.toml | 130 ++++++++++++++++++++++++------------------------- 1 file changed, 64 insertions(+), 66 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index da502848..b52c3ffc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,77 +1,84 @@ [build-system] build-backend = "setuptools.build_meta" requires = [ - "setuptools>=61.2", + "setuptools>=61.2", ] [project] +name = "fastkml" +version = "1.0.alpha.6" +description = "Fast KML processing in python" +[project.readme] +content-type = "text/x-rst" +text = "Introduction\n============\nKML is an XML geospatial data format and an `OGC standard `_\nthat deserves a canonical python implementation.\nFastkml is a library to read, write and manipulate KML files. It aims to keep\nit simple and fast (using lxml_ if available). Fast refers to the time you\nspend to write and read KML files as well as the time you spend to get\nacquainted to the library or to create KML objects. It aims to provide all of\nthe functionality that KML clients such as `OpenLayers\n`_, `Google Maps `_, and\n`Google Earth `_ provides.\nGeometries are handled as pygeoif_ objects.\n.. _pygeoif: http://pypi.python.org/pypi/pygeoif/\n.. _lxml: https://pypi.python.org/pypi/lxml\n.. _dateutils: https://pypi.python.org/pypi/dateutils\n.. _pip: https://pypi.python.org/pypi/pip\nFastkml is continually tested\n.. image:: https://github.com/cleder/fastkml/actions/workflows/run-all-tests.yml/badge.svg?branch=main\n:target: https://github.com/cleder/fastkml/actions/workflows/run-all-tests.yml\n:alt: Test\n.. image:: http://codecov.io/github/cleder/fastkml/coverage.svg?branch=main\n:target: http://codecov.io/github/cleder/fastkml?branch=main\n:alt: codecov.io\n.. image:: https://img.shields.io/badge/code+style-black-000000.svg\n:target: https://github.com/psf/black\n:alt: Black\nIs Maintained and documented:\n.. image:: https://img.shields.io/pypi/v/fastkml.svg\n:target: https://pypi.python.org/pypi/fastkml\n:alt: Latest PyPI version\n.. image:: https://img.shields.io/pypi/status/fastkml.svg\n:target: https://pypi.python.org/pypi/fastkml/\n:alt: Development Status\n.. image:: https://readthedocs.org/projects/fastkml/badge/\n:target: https://fastkml.readthedocs.org/\n:alt: Documentation\n.. image:: https://www.openhub.net/p/fastkml/widgets/project_thin_badge.gif\n:target: https://www.openhub.net/p/fastkml\n:alt: Statistics from OpenHub\n.. image:: https://img.shields.io/pypi/pyversions/fastkml.svg\n:target: https://pypi.python.org/pypi/fastkml/\n:alt: Supported Python versions\n.. image:: https://img.shields.io/pypi/implementation/fastkml.svg\n:target: https://pypi.python.org/pypi/fastkml/\n:alt: Supported Python implementations\nDocumentation\n=============\nYou can find all of the documentation for FastKML at `fastkml.readthedocs.org\n`_. If you find something that is missing,\nplease submit a pull request on `GitHub `_\nwith the improvement.\nHave a look at Aryan Guptas\n`The Definite Guide to FastKML. `_\nAlternatives\n============\n`Keytree `_ provides a less comprehensive, but more flexible\napproach.\nInstall\n========\nYou can install the package with ``pip install fastkml`` which will pull in all requirements.\nRequirements\n-------------\n* pygeoif_\n* dateutils_\nOptional\n---------\n* lxml_\nYou can install all of the requirements for working with FastKML by using\npip_::\npip install -r requirements.txt\nLimitations\n===========\n*Tesselate*, *Extrude* and *Altitude Mode* are assigned to a Geometry or\nGeometry collection (MultiGeometry). You cannot assign different values of\n*Tesselate*, *Extrude* or *Altitude Mode* on parts of a MultiGeometry.\nCurrently, the only major feature missing for the full Google Earth experience\nis the `gx extension\n`_.\nPlease submit a PR with the features you'd like to see implemented.\nChangelog\n=========\n1.0 (unreleased)\n-----------------\n- Drop Python 2 support\n- Use pygeoif >=1.0\n- Drop shapely native support\n- Add type annotations\n- refactor\n0.12 (2020/09/23)\n-----------------\n- add track and multi track [dericke]\n- remove travis, add github actions\n- raise error and debug log for no geometries found case [hyperknot]\n- protect AttributeError from data element without value [fpassaniti]\n- examples and fixes [heltonbiker]\n- improve documentation [whatnick]\n0.11.1 (2015/07/13)\n-------------------\n- add travis deploy to travis.yml\n0.11 (2015/07/10)\n-----------------\n- handle coordinates tuples which contain spaces\n0.10 (2015/06/09)\n-----------------\n- Fix bug when the fill or outline attributes of a PolyStyle are float strings\n0.9 (2014/10/17)\n-----------------\n- Add tox.ini for running tests using tox [Ian Lee]\n- Add documentation, hosted at https://fastkml.readthedocs.org [Ian Lee]\n0.8 (2014/09/18)\n-----------------\n- Add support for address and phoneNumber [Ian Lee]\n- Add support for Ground Overlay kml [Ian Lee]\n0.7 (2014/08/01)\n----------------\n- Handle case where Document booleans (visibility,isopen) are 'true' or 'false' [jwhelland]\n- test case additions and lxml warning [Ian Lee]\n- pep8-ify source code (except test_main.py) [Ian Lee]\n- pyflakes-ify source code (except __init__.py) [Ian Lee]\n0.6 (2014/05/29)\n----------------\n- add Schema\n- add SchemaData\n- make use of lxmls default namespace\n0.5 (2013/10/23)\n-----------------\n- handle big files with huge_tree for lxml [Egil Moeller]\n- bugfixes\n0.4 (2013/09/05)\n-----------------\n- adds the ability to add untyped extended data / named value pairs [Denis Krienbuehl]\n0.3 (2012/11/15)\n-----------------\n- specify minor python versions tested with Travis CI\n- add support for tesselation, altitudeMode and extrude to Geometries\n- move implementation of geometry from kml.Placemark to geometry.Geometry\n- add support for heterogenous GeometryCollection\n- python 3 compatible\n- fix test for python 3\n- change license to LGPL\n- register namespaces for a more pleasant, human readable xml output\n0.2 (2012/07/27)\n-----------------\n- remove dependency on shapely\n- add more functionality\n0.1.1 (2012/06/29)\n------------------\n- add MANIFEST.in\n0.1 (2012/06/27)\n----------------\n- initial release" + +keywords = [ + "GIS", + "Google", + "KML", + "Maps", + "OpenLayers", +] +[project.license] +text = "LGPL" + authors = [ { email = "christian.ledermann@gmail.com", name = "Christian Ledermann" }, ] +requires-python = ">=3.7" classifiers = [ - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Topic :: Scientific/Engineering :: GIS", + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Scientific/Engineering :: GIS", ] dependencies = [ - "pygeoif>=1.1.0", - "python-dateutil", - "setuptools", - "typing-extensions>4", -] -description = "Fast KML processing in python" -keywords = [ - "GIS", - "Google", - "KML", - "Maps", - "OpenLayers", + "pygeoif>=1.1", + "python-dateutil", + "setuptools", + "typing-extensions>4", ] -name = "fastkml" -requires-python = ">=3.7" -version = "1.0.alpha.6" - -[project.license] -text = "LGPL" - [project.optional-dependencies] testing = [ - "pytest", + "pytest", ] - -[project.readme] -content-type = "text/x-rst" -text = "Introduction\n============\nKML is an XML geospatial data format and an `OGC standard `_\nthat deserves a canonical python implementation.\nFastkml is a library to read, write and manipulate KML files. It aims to keep\nit simple and fast (using lxml_ if available). Fast refers to the time you\nspend to write and read KML files as well as the time you spend to get\nacquainted to the library or to create KML objects. It aims to provide all of\nthe functionality that KML clients such as `OpenLayers\n`_, `Google Maps `_, and\n`Google Earth `_ provides.\nGeometries are handled as pygeoif_ objects.\n.. _pygeoif: http://pypi.python.org/pypi/pygeoif/\n.. _lxml: https://pypi.python.org/pypi/lxml\n.. _dateutils: https://pypi.python.org/pypi/dateutils\n.. _pip: https://pypi.python.org/pypi/pip\nFastkml is continually tested\n.. image:: https://github.com/cleder/fastkml/actions/workflows/run-all-tests.yml/badge.svg?branch=main\n:target: https://github.com/cleder/fastkml/actions/workflows/run-all-tests.yml\n:alt: Test\n.. image:: http://codecov.io/github/cleder/fastkml/coverage.svg?branch=main\n:target: http://codecov.io/github/cleder/fastkml?branch=main\n:alt: codecov.io\n.. image:: https://img.shields.io/badge/code+style-black-000000.svg\n:target: https://github.com/psf/black\n:alt: Black\nIs Maintained and documented:\n.. image:: https://img.shields.io/pypi/v/fastkml.svg\n:target: https://pypi.python.org/pypi/fastkml\n:alt: Latest PyPI version\n.. image:: https://img.shields.io/pypi/status/fastkml.svg\n:target: https://pypi.python.org/pypi/fastkml/\n:alt: Development Status\n.. image:: https://readthedocs.org/projects/fastkml/badge/\n:target: https://fastkml.readthedocs.org/\n:alt: Documentation\n.. image:: https://www.openhub.net/p/fastkml/widgets/project_thin_badge.gif\n:target: https://www.openhub.net/p/fastkml\n:alt: Statistics from OpenHub\n.. image:: https://img.shields.io/pypi/pyversions/fastkml.svg\n:target: https://pypi.python.org/pypi/fastkml/\n:alt: Supported Python versions\n.. image:: https://img.shields.io/pypi/implementation/fastkml.svg\n:target: https://pypi.python.org/pypi/fastkml/\n:alt: Supported Python implementations\nDocumentation\n=============\nYou can find all of the documentation for FastKML at `fastkml.readthedocs.org\n`_. If you find something that is missing,\nplease submit a pull request on `GitHub `_\nwith the improvement.\nHave a look at Aryan Guptas\n`The Definite Guide to FastKML. `_\nAlternatives\n============\n`Keytree `_ provides a less comprehensive, but more flexible\napproach.\nInstall\n========\nYou can install the package with ``pip install fastkml`` which will pull in all requirements.\nRequirements\n-------------\n* pygeoif_\n* dateutils_\nOptional\n---------\n* lxml_\nYou can install all of the requirements for working with FastKML by using\npip_::\npip install -r requirements.txt\nLimitations\n===========\n*Tesselate*, *Extrude* and *Altitude Mode* are assigned to a Geometry or\nGeometry collection (MultiGeometry). You cannot assign different values of\n*Tesselate*, *Extrude* or *Altitude Mode* on parts of a MultiGeometry.\nCurrently, the only major feature missing for the full Google Earth experience\nis the `gx extension\n`_.\nPlease submit a PR with the features you'd like to see implemented.\nChangelog\n=========\n1.0 (unreleased)\n-----------------\n- Drop Python 2 support\n- Use pygeoif >=1.0\n- Drop shapely native support\n- Add type annotations\n- refactor\n0.12 (2020/09/23)\n-----------------\n- add track and multi track [dericke]\n- remove travis, add github actions\n- raise error and debug log for no geometries found case [hyperknot]\n- protect AttributeError from data element without value [fpassaniti]\n- examples and fixes [heltonbiker]\n- improve documentation [whatnick]\n0.11.1 (2015/07/13)\n-------------------\n- add travis deploy to travis.yml\n0.11 (2015/07/10)\n-----------------\n- handle coordinates tuples which contain spaces\n0.10 (2015/06/09)\n-----------------\n- Fix bug when the fill or outline attributes of a PolyStyle are float strings\n0.9 (2014/10/17)\n-----------------\n- Add tox.ini for running tests using tox [Ian Lee]\n- Add documentation, hosted at https://fastkml.readthedocs.org [Ian Lee]\n0.8 (2014/09/18)\n-----------------\n- Add support for address and phoneNumber [Ian Lee]\n- Add support for Ground Overlay kml [Ian Lee]\n0.7 (2014/08/01)\n----------------\n- Handle case where Document booleans (visibility,isopen) are 'true' or 'false' [jwhelland]\n- test case additions and lxml warning [Ian Lee]\n- pep8-ify source code (except test_main.py) [Ian Lee]\n- pyflakes-ify source code (except __init__.py) [Ian Lee]\n0.6 (2014/05/29)\n----------------\n- add Schema\n- add SchemaData\n- make use of lxmls default namespace\n0.5 (2013/10/23)\n-----------------\n- handle big files with huge_tree for lxml [Egil Moeller]\n- bugfixes\n0.4 (2013/09/05)\n-----------------\n- adds the ability to add untyped extended data / named value pairs [Denis Krienbuehl]\n0.3 (2012/11/15)\n-----------------\n- specify minor python versions tested with Travis CI\n- add support for tesselation, altitudeMode and extrude to Geometries\n- move implementation of geometry from kml.Placemark to geometry.Geometry\n- add support for heterogenous GeometryCollection\n- python 3 compatible\n- fix test for python 3\n- change license to LGPL\n- register namespaces for a more pleasant, human readable xml output\n0.2 (2012/07/27)\n-----------------\n- remove dependency on shapely\n- add more functionality\n0.1.1 (2012/06/29)\n------------------\n- add MANIFEST.in\n0.1 (2012/06/27)\n----------------\n- initial release" - [project.urls] Homepage = "https://github.com/cleder/fastkml" -[tool.check-manifest] -ignore = [ - ".*", - "examples/*", - "mutmut_config.py", - "test-requirements.txt", - "tox.ini", +[tool.setuptools] +include-package-data = true +zip-safe = false + +[tool.setuptools.packages.find] +exclude = [ + "ez_setup", ] +namespaces = false -[tool.flake8] -max_line_length = 89 +[tool.ruff.extend-per-file-ignores] +"setup.py" = [ + "E501", +] +"tests/oldunit_test.py" = [ + "E501", +] [tool.isort] force_single_line = true line_length = 88 +[tool.flake8] +max_line_length = 89 + [tool.mypy] disallow_any_generics = true disallow_incomplete_defs = true @@ -104,6 +111,15 @@ module = [ "fastkml.views", ] +[tool.check-manifest] +ignore = [ + ".*", + "examples/*", + "mutmut_config.py", + "test-requirements.txt", + "tox.ini", +] + [tool.pyright] exclude = [ "**/__pycache__", @@ -114,21 +130,3 @@ exclude = [ include = [ "fastkml", ] - -[tool.ruff.extend-per-file-ignores] -"setup.py" = [ - "E501", -] -"tests/oldunit_test.py" = [ - "E501", -] - -[tool.setuptools] -include-package-data = true -zip-safe = false - -[tool.setuptools.packages.find] -exclude = [ - "ez_setup", -] -namespaces = false From a5779c17e29ed8b53359f58764028e134f445abc Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 28 Oct 2023 20:47:31 +0100 Subject: [PATCH 06/63] fix pyproject.toml --- .pre-commit-config.yaml | 8 +-- README.rst | 12 +++- pyproject.toml | 121 ++++++++++++++++++++-------------------- setup.py | 70 ----------------------- 4 files changed, 76 insertions(+), 135 deletions(-) delete mode 100644 setup.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0df31dad..dc6c5acc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,11 +20,10 @@ repos: - id: pretty-format-json - id: requirements-txt-fixer - id: trailing-whitespace - - - repo: https://github.com/tox-dev/pyproject-fmt - rev: "1.2.0" + - repo: https://github.com/kieran-ryan/pyprojectsort + rev: v0.3.0 hooks: - - id: pyproject-fmt + - id: pyprojectsort - repo: https://github.com/abravalheri/validate-pyproject rev: v0.15 hooks: @@ -79,6 +78,7 @@ repos: - id: rst-directive-colons - id: rst-inline-touching-normal - id: text-unicode-replacement-char + # - repo: https://github.com/mgedmin/check-manifest # rev: "0.49" # hooks: diff --git a/README.rst b/README.rst index 6df6e595..6c93aa2e 100644 --- a/README.rst +++ b/README.rst @@ -30,7 +30,7 @@ Fastkml is continually tested :target: http://codecov.io/github/cleder/fastkml?branch=main :alt: codecov.io -.. image:: https://img.shields.io/badge/code+style-black-000000.svg +.. image:: https://img.shields.io/badge/code-style-black-000000.svg :target: https://github.com/psf/black :alt: Black @@ -44,6 +44,10 @@ Is Maintained and documented: :target: https://pypi.python.org/pypi/fastkml/ :alt: Development Status +.. image:: https://img.shields.io/pypi/l/fastkml + :target: https://www.gnu.org/licenses/lgpl-3.0.en.html + :alt: LGPL - License + .. image:: https://readthedocs.org/projects/fastkml/badge/ :target: https://fastkml.readthedocs.org/ :alt: Documentation @@ -60,6 +64,12 @@ Is Maintained and documented: :target: https://pypi.python.org/pypi/fastkml/ :alt: Supported Python implementations +.. image:: https://img.shields.io/librariesio/release/pypi/fastkml + :target: https://libraries.io/pypi/fastkml + :alt: Libraries.io dependency status for latest release + + + Documentation ============= diff --git a/pyproject.toml b/pyproject.toml index b52c3ffc..420ff3ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,84 +1,76 @@ [build-system] build-backend = "setuptools.build_meta" requires = [ - "setuptools>=61.2", + "setuptools>=61.2", ] [project] +description = "Fast KML processing in python" name = "fastkml" version = "1.0.alpha.6" -description = "Fast KML processing in python" -[project.readme] -content-type = "text/x-rst" -text = "Introduction\n============\nKML is an XML geospatial data format and an `OGC standard `_\nthat deserves a canonical python implementation.\nFastkml is a library to read, write and manipulate KML files. It aims to keep\nit simple and fast (using lxml_ if available). Fast refers to the time you\nspend to write and read KML files as well as the time you spend to get\nacquainted to the library or to create KML objects. It aims to provide all of\nthe functionality that KML clients such as `OpenLayers\n`_, `Google Maps `_, and\n`Google Earth `_ provides.\nGeometries are handled as pygeoif_ objects.\n.. _pygeoif: http://pypi.python.org/pypi/pygeoif/\n.. _lxml: https://pypi.python.org/pypi/lxml\n.. _dateutils: https://pypi.python.org/pypi/dateutils\n.. _pip: https://pypi.python.org/pypi/pip\nFastkml is continually tested\n.. image:: https://github.com/cleder/fastkml/actions/workflows/run-all-tests.yml/badge.svg?branch=main\n:target: https://github.com/cleder/fastkml/actions/workflows/run-all-tests.yml\n:alt: Test\n.. image:: http://codecov.io/github/cleder/fastkml/coverage.svg?branch=main\n:target: http://codecov.io/github/cleder/fastkml?branch=main\n:alt: codecov.io\n.. image:: https://img.shields.io/badge/code+style-black-000000.svg\n:target: https://github.com/psf/black\n:alt: Black\nIs Maintained and documented:\n.. image:: https://img.shields.io/pypi/v/fastkml.svg\n:target: https://pypi.python.org/pypi/fastkml\n:alt: Latest PyPI version\n.. image:: https://img.shields.io/pypi/status/fastkml.svg\n:target: https://pypi.python.org/pypi/fastkml/\n:alt: Development Status\n.. image:: https://readthedocs.org/projects/fastkml/badge/\n:target: https://fastkml.readthedocs.org/\n:alt: Documentation\n.. image:: https://www.openhub.net/p/fastkml/widgets/project_thin_badge.gif\n:target: https://www.openhub.net/p/fastkml\n:alt: Statistics from OpenHub\n.. image:: https://img.shields.io/pypi/pyversions/fastkml.svg\n:target: https://pypi.python.org/pypi/fastkml/\n:alt: Supported Python versions\n.. image:: https://img.shields.io/pypi/implementation/fastkml.svg\n:target: https://pypi.python.org/pypi/fastkml/\n:alt: Supported Python implementations\nDocumentation\n=============\nYou can find all of the documentation for FastKML at `fastkml.readthedocs.org\n`_. If you find something that is missing,\nplease submit a pull request on `GitHub `_\nwith the improvement.\nHave a look at Aryan Guptas\n`The Definite Guide to FastKML. `_\nAlternatives\n============\n`Keytree `_ provides a less comprehensive, but more flexible\napproach.\nInstall\n========\nYou can install the package with ``pip install fastkml`` which will pull in all requirements.\nRequirements\n-------------\n* pygeoif_\n* dateutils_\nOptional\n---------\n* lxml_\nYou can install all of the requirements for working with FastKML by using\npip_::\npip install -r requirements.txt\nLimitations\n===========\n*Tesselate*, *Extrude* and *Altitude Mode* are assigned to a Geometry or\nGeometry collection (MultiGeometry). You cannot assign different values of\n*Tesselate*, *Extrude* or *Altitude Mode* on parts of a MultiGeometry.\nCurrently, the only major feature missing for the full Google Earth experience\nis the `gx extension\n`_.\nPlease submit a PR with the features you'd like to see implemented.\nChangelog\n=========\n1.0 (unreleased)\n-----------------\n- Drop Python 2 support\n- Use pygeoif >=1.0\n- Drop shapely native support\n- Add type annotations\n- refactor\n0.12 (2020/09/23)\n-----------------\n- add track and multi track [dericke]\n- remove travis, add github actions\n- raise error and debug log for no geometries found case [hyperknot]\n- protect AttributeError from data element without value [fpassaniti]\n- examples and fixes [heltonbiker]\n- improve documentation [whatnick]\n0.11.1 (2015/07/13)\n-------------------\n- add travis deploy to travis.yml\n0.11 (2015/07/10)\n-----------------\n- handle coordinates tuples which contain spaces\n0.10 (2015/06/09)\n-----------------\n- Fix bug when the fill or outline attributes of a PolyStyle are float strings\n0.9 (2014/10/17)\n-----------------\n- Add tox.ini for running tests using tox [Ian Lee]\n- Add documentation, hosted at https://fastkml.readthedocs.org [Ian Lee]\n0.8 (2014/09/18)\n-----------------\n- Add support for address and phoneNumber [Ian Lee]\n- Add support for Ground Overlay kml [Ian Lee]\n0.7 (2014/08/01)\n----------------\n- Handle case where Document booleans (visibility,isopen) are 'true' or 'false' [jwhelland]\n- test case additions and lxml warning [Ian Lee]\n- pep8-ify source code (except test_main.py) [Ian Lee]\n- pyflakes-ify source code (except __init__.py) [Ian Lee]\n0.6 (2014/05/29)\n----------------\n- add Schema\n- add SchemaData\n- make use of lxmls default namespace\n0.5 (2013/10/23)\n-----------------\n- handle big files with huge_tree for lxml [Egil Moeller]\n- bugfixes\n0.4 (2013/09/05)\n-----------------\n- adds the ability to add untyped extended data / named value pairs [Denis Krienbuehl]\n0.3 (2012/11/15)\n-----------------\n- specify minor python versions tested with Travis CI\n- add support for tesselation, altitudeMode and extrude to Geometries\n- move implementation of geometry from kml.Placemark to geometry.Geometry\n- add support for heterogenous GeometryCollection\n- python 3 compatible\n- fix test for python 3\n- change license to LGPL\n- register namespaces for a more pleasant, human readable xml output\n0.2 (2012/07/27)\n-----------------\n- remove dependency on shapely\n- add more functionality\n0.1.1 (2012/06/29)\n------------------\n- add MANIFEST.in\n0.1 (2012/06/27)\n----------------\n- initial release" -keywords = [ - "GIS", - "Google", - "KML", - "Maps", - "OpenLayers", -] [project.license] -text = "LGPL" - authors = [ { email = "christian.ledermann@gmail.com", name = "Christian Ledermann" }, ] -requires-python = ">=3.7" classifiers = [ - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Topic :: Scientific/Engineering :: GIS", + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Scientific/Engineering :: GIS", ] dependencies = [ - "pygeoif>=1.1", - "python-dateutil", - "setuptools", - "typing-extensions>4", + "pygeoif>=1.1", + "python-dateutil", + "typing-extensions>4", ] +requires-python = ">=3.7" +text = "LGPL" + [project.optional-dependencies] testing = [ - "pytest", + "pytest", ] + +[project.readme] +content-type = "text/x-rst" +file = "README.rst" +keywords = [ + "GIS", + "Google", + "KML", + "Maps", + "OpenLayers", +] + [project.urls] Homepage = "https://github.com/cleder/fastkml" -[tool.setuptools] -include-package-data = true -zip-safe = false - -[tool.setuptools.packages.find] -exclude = [ - "ez_setup", +[tool.check-manifest] +ignore = [ + ".*", + "examples/*", + "mutmut_config.py", + "test-requirements.txt", + "tox.ini", ] -namespaces = false -[tool.ruff.extend-per-file-ignores] -"setup.py" = [ - "E501", -] -"tests/oldunit_test.py" = [ - "E501", -] +[tool.flake8] +max_line_length = 89 [tool.isort] force_single_line = true line_length = 88 -[tool.flake8] -max_line_length = 89 - [tool.mypy] disallow_any_generics = true disallow_incomplete_defs = true @@ -111,15 +103,6 @@ module = [ "fastkml.views", ] -[tool.check-manifest] -ignore = [ - ".*", - "examples/*", - "mutmut_config.py", - "test-requirements.txt", - "tox.ini", -] - [tool.pyright] exclude = [ "**/__pycache__", @@ -130,3 +113,21 @@ exclude = [ include = [ "fastkml", ] + +[tool.ruff.extend-per-file-ignores] +"setup.py" = [ + "E501", +] +"tests/oldunit_test.py" = [ + "E501", +] + +[tool.setuptools] +include-package-data = true +zip-safe = false + +[tool.setuptools.packages.find] +exclude = [ + "ez_setup", +] +namespaces = false diff --git a/setup.py b/setup.py deleted file mode 100644 index 175b8db7..00000000 --- a/setup.py +++ /dev/null @@ -1,70 +0,0 @@ -import os -import sys - -from setuptools import find_packages -from setuptools import setup -from setuptools.command.test import test as TestCommand - - -class PyTest(TestCommand): - def finalize_options(self) -> None: - TestCommand.finalize_options(self) - self.test_args = [] - self.test_suite = True - - def run_tests(self) -> None: - # import here, cause outside the eggs aren't loaded - import pytest - - errno = pytest.main(self.test_args) - sys.exit(errno) - - -setup( - name="fastkml", - version="1.0.alpha.6", - description="Fast KML processing in python", - long_description=( - open("README.rst").read() - + "\n" - + open(os.path.join("docs", "HISTORY.txt")).read() - ), - long_description_content_type="text/x-rst", - classifiers=[ - "Topic :: Scientific/Engineering :: GIS", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Intended Audience :: Developers", - "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", - # "Development Status :: 5 - Production/Stable", - "Development Status :: 3 - Alpha", - "Operating System :: OS Independent", - ], - keywords="GIS KML Google Maps OpenLayers", - author="Christian Ledermann", - author_email="christian.ledermann@gmail.com", - url="https://github.com/cleder/fastkml", - license="LGPL", - packages=find_packages(exclude=["ez_setup", "examples", "tests"]), - include_package_data=True, - zip_safe=False, - tests_require=["pytest"], - cmdclass={"test": PyTest}, - python_requires=">=3.7", - install_requires=[ - # -*- Extra requirements: -*- - "pygeoif>=1.1.0", - "python-dateutil", - "setuptools", - "typing-extensions>4", - ], - entry_points=""" - # -*- Entry points: -*- - """, -) From 176c1b54f591e5bf37a13b87120fb55ef7b7bb71 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 28 Oct 2023 20:57:47 +0100 Subject: [PATCH 07/63] update python version for static tests --- .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 ead106f4..54f48cbb 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -56,7 +56,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.9'] + python-version: ['3.11'] steps: - uses: actions/checkout@v4 From 05db0267f251e629496e3be35e30ca49da2b0f69 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 28 Oct 2023 21:04:12 +0100 Subject: [PATCH 08/63] fix pypy test command --- .github/workflows/run-all-tests.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index 54f48cbb..615d7394 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -51,6 +51,7 @@ jobs: with: fail_ci_if_error: true verbose: true + token: ${{ env.CODECOV_TOKEN }} static-tests: runs-on: ubuntu-latest @@ -97,9 +98,10 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip wheel + pip install pytest - name: Test with pytest run: | - python setup.py test + pytest tests publish: if: "github.event_name == 'push' && github.repository == 'cleder/fastkml'" From debc57f8f5c79fb707c806c536c39d0f572cb481 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 28 Oct 2023 21:07:44 +0100 Subject: [PATCH 09/63] fix pypy install test dependencies --- .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 615d7394..e8d4fefc 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -98,7 +98,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip wheel - pip install pytest + python -m pip install -e ".[tests]" - name: Test with pytest run: | pytest tests From 13f5a12c06efd546d302f9737b8030e22cf3f965 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 28 Oct 2023 21:10:13 +0100 Subject: [PATCH 10/63] dependency test, not testing --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 420ff3ff..e0130c87 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ requires-python = ">=3.7" text = "LGPL" [project.optional-dependencies] -testing = [ +tests = [ "pytest", ] From 83f9effad8b3ada69f2abcb9f8cd7bb861009b16 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 28 Oct 2023 21:25:33 +0100 Subject: [PATCH 11/63] WTF are dependencies ignored on pypy --- pyproject.toml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index e0130c87..853fec45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,8 +37,24 @@ requires-python = ">=3.7" text = "LGPL" [project.optional-dependencies] +complexity = [ + "lizard", + "radon", +] +dev = [ + "fastkml[complexity]", + "fastkml[tests]", + "fastkml[typing]", + "pre-commit", +] tests = [ "pytest", + "pytest-cov", +] +typing = [ + "mypy", + "types-python-dateutil", + "types-setuptools", ] [project.readme] From f427e1af059cc6f7ac933ff810f3c4a5fde0c77e Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 28 Oct 2023 21:33:37 +0100 Subject: [PATCH 12/63] fix pyproject.toml --- .github/workflows/run-all-tests.yml | 7 +++---- pyproject.toml | 13 ++++++++----- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index e8d4fefc..dc7b0319 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -20,7 +20,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip wheel - pip install -r test-requirements.txt + pip install -e ".[tests]" - name: Test with pytest run: | pytest tests @@ -40,8 +40,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip wheel - pip install -r test-requirements.txt - pip install lxml + pip install -e ".[tests, lxml]" - name: Test with pytest run: | pytest tests --cov=fastkml --cov=tests --cov-fail-under=88 --cov-report=xml @@ -97,7 +96,7 @@ jobs: python-version: ${{ matrix.pypy-version }} - name: Install dependencies run: | - python -m pip install --upgrade pip wheel + python -m pip install --upgrade pip wheel setuptools python -m pip install -e ".[tests]" - name: Test with pytest run: | diff --git a/pyproject.toml b/pyproject.toml index 853fec45..7de859e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,11 +5,6 @@ requires = [ ] [project] -description = "Fast KML processing in python" -name = "fastkml" -version = "1.0.alpha.6" - -[project.license] authors = [ { email = "christian.ledermann@gmail.com", name = "Christian Ledermann" }, ] @@ -33,7 +28,12 @@ dependencies = [ "python-dateutil", "typing-extensions>4", ] +description = "Fast KML processing in python" +name = "fastkml" requires-python = ">=3.7" +version = "1.0.alpha.6" + +[project.license] text = "LGPL" [project.optional-dependencies] @@ -47,6 +47,9 @@ dev = [ "fastkml[typing]", "pre-commit", ] +lxml = [ + "lxml", +] tests = [ "pytest", "pytest-cov", From f1ecd388407dc57ddde1bccd40a394343f4476b8 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 28 Oct 2023 21:43:36 +0100 Subject: [PATCH 13/63] dynamic versioning --- fastkml/__init__.py | 10 +--------- fastkml/about.py | 6 ++++++ pyproject.toml | 8 ++++++-- 3 files changed, 13 insertions(+), 11 deletions(-) create mode 100644 fastkml/about.py diff --git a/fastkml/__init__.py b/fastkml/__init__.py index 4f603f57..4b7645cb 100644 --- a/fastkml/__init__.py +++ b/fastkml/__init__.py @@ -23,10 +23,7 @@ google maps rather than to give you all functionality that KML on google earth provides. """ - -from pkg_resources import DistributionNotFound -from pkg_resources import get_distribution - +from fastkml.about import __version__ # noqa: F401 from fastkml.atom import Author from fastkml.atom import Contributor from fastkml.atom import Link @@ -51,11 +48,6 @@ from fastkml.views import Camera from fastkml.views import LookAt -try: - __version__ = get_distribution("fastkml").version -except DistributionNotFound: - __version__ = "dev" - __all__ = [ "KML", "Document", diff --git a/fastkml/about.py b/fastkml/about.py new file mode 100644 index 00000000..53116a36 --- /dev/null +++ b/fastkml/about.py @@ -0,0 +1,6 @@ +""" +About fastkml + +The only purpose of this module is to provide a version number for the package. +""" +__version__ = "1.0.a7" diff --git a/pyproject.toml b/pyproject.toml index 7de859e9..df684c10 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,9 +29,11 @@ dependencies = [ "typing-extensions>4", ] description = "Fast KML processing in python" +dynamic = [ + "version", +] name = "fastkml" requires-python = ">=3.7" -version = "1.0.alpha.6" [project.license] text = "LGPL" @@ -57,7 +59,6 @@ tests = [ typing = [ "mypy", "types-python-dateutil", - "types-setuptools", ] [project.readme] @@ -145,6 +146,9 @@ include = [ include-package-data = true zip-safe = false +[tool.setuptools.dynamic.version] +attr = "fastkml.about.__version__" + [tool.setuptools.packages.find] exclude = [ "ez_setup", From 706ff68520314f832785bcb8f17520ba096ba086 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 28 Oct 2023 22:13:25 +0100 Subject: [PATCH 14/63] improve README --- README.rst | 22 ++++++++++++++-------- pyproject.toml | 1 + 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index 6c93aa2e..886afe77 100644 --- a/README.rst +++ b/README.rst @@ -15,11 +15,6 @@ the functionality that KML clients such as `OpenLayers Geometries are handled as pygeoif_ objects. -.. _pygeoif: http://pypi.python.org/pypi/pygeoif/ -.. _lxml: https://pypi.python.org/pypi/lxml -.. _dateutils: https://pypi.python.org/pypi/dateutils -.. _pip: https://pypi.python.org/pypi/pip - Fastkml is continually tested .. image:: https://github.com/cleder/fastkml/actions/workflows/run-all-tests.yml/badge.svg?branch=main @@ -30,10 +25,18 @@ Fastkml is continually tested :target: http://codecov.io/github/cleder/fastkml?branch=main :alt: codecov.io -.. image:: https://img.shields.io/badge/code-style-black-000000.svg +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black :alt: Black +.. image:: https://img.shields.io/badge/type%20checker-mypy-blue + :target: http://mypy-lang.org/ + :alt: Mypy + +.. image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit + :target: https://github.com/pre-commit/pre-commit + :alt: pre-commit + Is Maintained and documented: .. image:: https://img.shields.io/pypi/v/fastkml.svg @@ -68,8 +71,6 @@ Is Maintained and documented: :target: https://libraries.io/pypi/fastkml :alt: Libraries.io dependency status for latest release - - Documentation ============= @@ -120,3 +121,8 @@ Currently, the only major feature missing for the full Google Earth experience is the `gx extension `_. Please submit a PR with the features you'd like to see implemented. + +.. _pygeoif: http://pypi.python.org/pypi/pygeoif/ +.. _lxml: https://pypi.python.org/pypi/lxml +.. _dateutils: https://pypi.python.org/pypi/dateutils +.. _pip: https://pypi.python.org/pypi/pip diff --git a/pyproject.toml b/pyproject.toml index df684c10..f3f8d8c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,6 +73,7 @@ keywords = [ ] [project.urls] +Documentation = "https://fastkml.readthedocs.org/" Homepage = "https://github.com/cleder/fastkml" [tool.check-manifest] From 31471e89db86b9cf973541c7f0022ab008c47399 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 28 Oct 2023 22:52:28 +0100 Subject: [PATCH 15/63] fix readthedocs --- .readthedocs.yaml | 23 +++++++++++++++++++ doc-requirements.txt => docs/requirements.txt | 0 2 files changed, 23 insertions(+) create mode 100644 .readthedocs.yaml rename doc-requirements.txt => docs/requirements.txt (100%) diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..0a3b1864 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,23 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details +--- +# Required +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/conf.py + +# We recommend specifying your dependencies to enable reproducible builds: +# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: docs/requirements.txt +... diff --git a/doc-requirements.txt b/docs/requirements.txt similarity index 100% rename from doc-requirements.txt rename to docs/requirements.txt From de5c9f0e37f8ca05fdd3432e9c83add97fc24945 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 28 Oct 2023 22:59:44 +0100 Subject: [PATCH 16/63] requirements for read the docs --- docs/requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/requirements.txt b/docs/requirements.txt index 101ec4b5..421ba9e5 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,5 @@ # Documentation Requirements Sphinx +sphinx-autodoc-typehints sphinx-rtd-theme +typing_extensions From 401c43bb65984f7a246a441c47f5af238fc66ce1 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 28 Oct 2023 23:03:28 +0100 Subject: [PATCH 17/63] requirements for read the docs --- docs/requirements.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 421ba9e5..f8684331 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,7 @@ +pygeoif>=1.1 +python-dateutil # Documentation Requirements Sphinx sphinx-autodoc-typehints sphinx-rtd-theme -typing_extensions +typing-extensions>4 From 3b878a1deeb4f016cba29ae6008953e3b43d9e8d Mon Sep 17 00:00:00 2001 From: Alexandr Ovsyannikov Date: Sun, 29 Oct 2023 02:04:04 +0400 Subject: [PATCH 18/63] Change passive voice to active voice for reader's convenience. --- fastkml/atom.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/fastkml/atom.py b/fastkml/atom.py index a07be3b1..8a58d17d 100644 --- a/fastkml/atom.py +++ b/fastkml/atom.py @@ -59,9 +59,8 @@ def check_email(email: str) -> bool: class Link(_XMLObject): """ - Identifies a related Web page. The type of relation is defined by - the rel attribute. A feed is limited to one alternate per type and - hreflang. + Identifies a related Web page. The rel attribute defines the type of relation. + A feed is limited to one alternate per type and hreflang. is patterned after html's link element. It has one required attribute, href, and five optional attributes: rel, type, hreflang, title, and length. From e6406aa89c04589196ba85685bf389d645dd3fcf Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sun, 29 Oct 2023 18:29:50 +0000 Subject: [PATCH 19/63] sphinx autodoc --- docs/conf.py | 6 +- docs/fastkml.rst | 142 +++++++++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 42 +------------- docs/modules.rst | 7 +++ 4 files changed, 154 insertions(+), 43 deletions(-) create mode 100644 docs/fastkml.rst create mode 100644 docs/modules.rst diff --git a/docs/conf.py b/docs/conf.py index c87da42f..1cd2dcf1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,7 +18,7 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath("..")) -import fastkml # noqa: E402 +from fastkml import about # noqa: E402 # -- General configuration ------------------------------------------------ @@ -51,9 +51,9 @@ # built documents. # # The short X.Y version. -version = fastkml.__version__ +version = about.__version__ # The full version, including alpha/beta/rc tags. -release = fastkml.__version__ +release = about.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/fastkml.rst b/docs/fastkml.rst new file mode 100644 index 00000000..b218e7db --- /dev/null +++ b/docs/fastkml.rst @@ -0,0 +1,142 @@ +fastkml package +=============== + +Module contents +--------------- + +.. automodule:: fastkml + :members: + :undoc-members: + :noindex: + + +Submodules +---------- + +fastkml.about module +-------------------- + +.. automodule:: fastkml.about + :members: + :undoc-members: + :show-inheritance: + +fastkml.atom module +------------------- + +.. automodule:: fastkml.atom + :members: + :undoc-members: + :show-inheritance: + +fastkml.base module +------------------- + +.. automodule:: fastkml.base + :members: + :undoc-members: + :show-inheritance: + +fastkml.config module +--------------------- + +.. automodule:: fastkml.config + :members: + :undoc-members: + :show-inheritance: + +fastkml.data module +------------------- + +.. automodule:: fastkml.data + :members: + :undoc-members: + :show-inheritance: + +fastkml.enums module +-------------------- + +.. automodule:: fastkml.enums + :members: + :undoc-members: + :show-inheritance: + +fastkml.exceptions module +------------------------- + +.. automodule:: fastkml.exceptions + :members: + :undoc-members: + :show-inheritance: + +fastkml.geometry module +----------------------- + +.. automodule:: fastkml.geometry + :members: + :undoc-members: + :show-inheritance: + +fastkml.gx module +----------------- + +.. automodule:: fastkml.gx + :members: + :undoc-members: + :show-inheritance: + +fastkml.helpers module +---------------------- + +.. automodule:: fastkml.helpers + :members: + :undoc-members: + :show-inheritance: + +fastkml.kml module +------------------ + +.. automodule:: fastkml.kml + :members: + :undoc-members: + :show-inheritance: + +fastkml.mixins module +--------------------- + +.. automodule:: fastkml.mixins + :members: + :undoc-members: + :show-inheritance: + +fastkml.styles module +--------------------- + +.. automodule:: fastkml.styles + :members: + :undoc-members: + :show-inheritance: + +fastkml.times module +-------------------- + +.. automodule:: fastkml.times + :members: + :undoc-members: + :show-inheritance: + +fastkml.types module +-------------------- + +.. automodule:: fastkml.types + :members: + :undoc-members: + :show-inheritance: + +fastkml.views module +-------------------- + +.. automodule:: fastkml.views + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/index.rst b/docs/index.rst index 45b422b6..f9c92016 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,46 +1,7 @@ Welcome to FastKML's documentation! =================================== -Fastkml is continually tested with *Travis CI*: - -.. image:: https://api.travis-ci.org/cleder/fastkml.png - :target: https://travis-ci.org/cleder/fastkml - -.. image:: https://coveralls.io/repos/cleder/fastkml/badge.png?branch=master - :target: https://coveralls.io/r/cleder/fastkml?branch=master - - -Is Maintained and documented: - -.. image:: https://pypip.in/v/fastkml/badge.png - :target: https://pypi.python.org/pypi/fastkml - :alt: Latest PyPI version - -.. image:: https://pypip.in/status/fastkml/badge.svg - :target: https://pypi.python.org/pypi/fastkml/ - :alt: Development Status - -.. image:: https://readthedocs.org/projects/fastkml/badge/ - :target: https://fastkml.readthedocs.org/ - -.. image:: https://www.openhub.net/p/fastkml/widgets/project_thin_badge.gif - :target: https://www.openhub.net/p/fastkml - -Follows best practises: - -.. image:: https://landscape.io/github/cleder/fastkml/master/landscape.svg?style=plastic - :target: https://landscape.io/github/cleder/fastkml/master - :alt: Code Health - -.. image:: https://pypip.in/py_versions/fastkml/badge.svg - :target: https://pypi.python.org/pypi/fastkml/ - :alt: Supported Python versions - -.. image:: https://pypip.in/implementation/fastkml/badge.svg - :target: https://pypi.python.org/pypi/fastkml/ - :alt: Supported Python implementations - -fastkml is a library to read, write and manipulate KML files. It aims to keep +``fastkml`` is a library to read, write and manipulate KML files. It aims to keep it simple and fast (using lxml_ if available). "Fast" refers to the time you spend to write and read KML files, as well as the time you spend to get acquainted with the library or to create KML objects. It provides a subset of KML @@ -74,6 +35,7 @@ requirements, namely: installing usage_guide reference_guide + modules contributing .. _lxml: https://pypi.python.org/pypi/lxml diff --git a/docs/modules.rst b/docs/modules.rst new file mode 100644 index 00000000..8af3d880 --- /dev/null +++ b/docs/modules.rst @@ -0,0 +1,7 @@ +fastkml +======= + +.. toctree:: + :maxdepth: 4 + + fastkml From 0b40936a010c54b9c052e9a9c497b79962f35061 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Mon, 30 Oct 2023 15:01:10 +0000 Subject: [PATCH 20/63] fix github action, fix flake8 --- .github/workflows/run-all-tests.yml | 8 ++++---- fastkml/config.py | 3 --- fastkml/data.py | 20 ++++++++++++++++++++ fastkml/enums.py | 2 ++ fastkml/exceptions.py | 3 +++ fastkml/geometry.py | 12 ++++++++++++ fastkml/gx.py | 11 +++++++++++ fastkml/helpers.py | 8 ++++++++ pyproject.toml | 21 +++++++++++++++++++++ test-requirements.txt | 2 +- tox.ini | 4 +++- 11 files changed, 85 insertions(+), 9 deletions(-) diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index dc7b0319..ef0e9b03 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.7', '3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v4 @@ -29,7 +29,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v4 @@ -67,7 +67,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip wheel - pip install -r test-requirements.txt + pip install -e ".[typing, complexity, linting]" - name: Typecheck run: | mypy fastkml @@ -87,7 +87,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - pypy-version: ['pypy-3.7', 'pypy-3.8', 'pypy-3.9', 'pypy-3.10'] + pypy-version: ['pypy-3.8', 'pypy-3.9', 'pypy-3.10'] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.pypy-version }} diff --git a/fastkml/config.py b/fastkml/config.py index 699ce5a3..fd9dd08f 100644 --- a/fastkml/config.py +++ b/fastkml/config.py @@ -22,7 +22,6 @@ __all__ = [ "ATOMNS", "DEFAULT_NAME_SPACES", - "FORCE3D", "GXNS", "KMLNS", "register_namespaces", @@ -76,5 +75,3 @@ def set_default_namespaces() -> None: set_default_namespaces() - -FORCE3D = False diff --git a/fastkml/data.py b/fastkml/data.py index 72a5bb7a..49d8169b 100644 --- a/fastkml/data.py +++ b/fastkml/data.py @@ -30,6 +30,26 @@ from fastkml.enums import Verbosity from fastkml.types import Element +__all__ = [ + "Data", + "ExtendedData", + "Schema", + "SchemaData", + "SchemaDataDictInput", + "SchemaDataInput", + "SchemaDataListInput", + "SchemaDataOutput", + "SchemaDataTupleInput", + "SchemaDataType", + "SimpleField", + "SimpleFields", + "SimpleFieldsDictInput", + "SimpleFieldsInput", + "SimpleFieldsListInput", + "SimpleFieldsOutput", + "SimpleFieldsTupleInput", +] + logger = logging.getLogger(__name__) diff --git a/fastkml/enums.py b/fastkml/enums.py index 8c1ab589..25d1d802 100644 --- a/fastkml/enums.py +++ b/fastkml/enums.py @@ -16,6 +16,8 @@ from enum import Enum from enum import unique +__all__ = ["AltitudeMode", "DateTimeResolution", "Verbosity"] + @unique class Verbosity(Enum): diff --git a/fastkml/exceptions.py b/fastkml/exceptions.py index 600aaee3..51e1bcde 100644 --- a/fastkml/exceptions.py +++ b/fastkml/exceptions.py @@ -26,3 +26,6 @@ class KMLParseError(FastKMLError): class KMLWriteError(FastKMLError): """Raised when there is an error writing KML.""" + + +__all__ = ["FastKMLError", "KMLParseError", "KMLWriteError"] diff --git a/fastkml/geometry.py b/fastkml/geometry.py index 48575d44..aa7b1f5e 100644 --- a/fastkml/geometry.py +++ b/fastkml/geometry.py @@ -37,6 +37,18 @@ from fastkml.exceptions import KMLWriteError from fastkml.types import Element +__all__ = [ + "AnyGeometryType", + "GeometryType", + "LineString", + "LinearRing", + "MultiGeometry", + "MultiGeometryType", + "Point", + "Polygon", + "create_multigeometry", +] + logger = logging.getLogger(__name__) GeometryType = Union[geo.Polygon, geo.LineString, geo.LinearRing, geo.Point] diff --git a/fastkml/gx.py b/fastkml/gx.py index 892850e9..db5f2e11 100644 --- a/fastkml/gx.py +++ b/fastkml/gx.py @@ -97,6 +97,17 @@ from fastkml.geometry import _Geometry from fastkml.types import Element +__all__ = [ + "Angle", + "MultiTrack", + "Track", + "TrackItem", + "linestring_to_track_items", + "multilinestring_to_tracks", + "track_items_to_geometry", + "tracks_to_geometry", +] + logger = logging.getLogger(__name__) diff --git a/fastkml/helpers.py b/fastkml/helpers.py index 781ea623..5c678a00 100644 --- a/fastkml/helpers.py +++ b/fastkml/helpers.py @@ -23,6 +23,14 @@ from fastkml import config from fastkml.types import Element +__all__ = [ + "o_from_attr", + "o_from_subelement_text", + "o_int_from_attr", + "o_to_attr", + "o_to_subelement_text", +] + logger = logging.getLogger(__name__) diff --git a/pyproject.toml b/pyproject.toml index f3f8d8c5..3b411af0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,10 +45,31 @@ complexity = [ ] dev = [ "fastkml[complexity]", + "fastkml[docs]", "fastkml[tests]", "fastkml[typing]", "pre-commit", ] +docs = [ + "Sphinx", + "sphinx-autodoc-typehints", + "sphinx-rtd-theme", +] +linting = [ + "black", + "flake8", + "flake8-comments", + "flake8-continuation", + "flake8-encodings", + "flake8-expression-complexity", + "flake8-length", + "flake8-literal", + "flake8-pep3101", + "flake8-super", + "flake8-typing-imports", + "ruff", + "yamllint", +] lxml = [ "lxml", ] diff --git a/test-requirements.txt b/test-requirements.txt index 48485321..5f7419e4 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,5 +1,5 @@ -r requirements.txt --r doc-requirements.txt +-r doc/requirements.txt black flake8 # flake8-dunder-all diff --git a/tox.ini b/tox.ini index 4c5bbca5..4145a018 100644 --- a/tox.ini +++ b/tox.ini @@ -26,7 +26,7 @@ deps = commands = make html [flake8] -min_python_version = 3.6.10 +min_python_version = 3.7 exclude = .git,__pycache__,docs/source/conf.py,old,build,dist max_line_length = 89 ignore= @@ -35,3 +35,5 @@ per-file-ignores = tests/*.py: E722,E741,E501,DALL examples/*.py: DALL enable-extensions=G +literal_inline_quotes = double +literal_multiline_quotes = double From d3fde9df3d9153c3af338c021320878e8a1242cf Mon Sep 17 00:00:00 2001 From: Hamed Faramarzi Date: Mon, 30 Oct 2023 01:46:32 +0100 Subject: [PATCH 21/63] feat: Use arrow instead of dateutil Signed-off-by: Hamed Faramarzi --- README.rst | 1 + docs/requirements.txt | 2 +- fastkml/gx.py | 4 ++-- fastkml/times.py | 15 ++++++--------- pyproject.toml | 2 +- requirements.txt | 2 +- test-requirements.txt | 1 + tests/times_test.py | 22 +++++++++++----------- tests/views_test.py | 20 ++++++++++---------- 9 files changed, 34 insertions(+), 35 deletions(-) diff --git a/README.rst b/README.rst index 886afe77..d398070c 100644 --- a/README.rst +++ b/README.rst @@ -98,6 +98,7 @@ Requirements * pygeoif_ * dateutils_ +* arrow_ Optional --------- diff --git a/docs/requirements.txt b/docs/requirements.txt index f8684331..57e58a5f 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ +arrow pygeoif>=1.1 -python-dateutil # Documentation Requirements Sphinx sphinx-autodoc-typehints diff --git a/fastkml/gx.py b/fastkml/gx.py index 892850e9..090095f2 100644 --- a/fastkml/gx.py +++ b/fastkml/gx.py @@ -88,7 +88,7 @@ from typing import Sequence from typing import cast -import dateutil.parser +import arrow import pygeoif.geometry as geo import fastkml.config as config @@ -247,7 +247,7 @@ def track_items_kwargs_from_element( time_stamps: List[Optional[datetime.datetime]] = [] for time_stamp in element.findall(f"{config.KMLNS}when"): if time_stamp is not None and time_stamp.text: - time_stamps.append(dateutil.parser.parse(time_stamp.text)) + time_stamps.append(arrow.get(time_stamp.text).datetime) else: time_stamps.append(None) coords: List[Optional[geo.Point]] = [] diff --git a/fastkml/times.py b/fastkml/times.py index ccca30f1..707a5589 100644 --- a/fastkml/times.py +++ b/fastkml/times.py @@ -20,11 +20,8 @@ from typing import Optional from typing import Union -# note that there are some ISO 8601 timeparsers at pypi -# but in my tests all of them had some errors so we rely on the -# tried and tested dateutil here which is more stable. As a side effect -# we can also parse non ISO compliant dateTimes -import dateutil.parser +import arrow +from dateutil import parser import fastkml.config as config from fastkml.base import _BaseObject @@ -126,20 +123,20 @@ def parse(cls, datestr: str) -> Optional["KmlDateTime"]: dt = None if len(datestr) == 4: year = int(datestr) - dt = datetime(year, 1, 1) + dt = arrow.get(year, 1, 1).datetime resolution = DateTimeResolution.year elif len(datestr) in {6, 7}: ym = year_month.match(datestr) if ym: year = int(ym.group("year")) month = int(ym.group("month")) - dt = datetime(year, month, 1) + dt = arrow.get(year, month, 1).datetime resolution = DateTimeResolution.year_month elif len(datestr) in {8, 10}: # 8 is YYYYMMDD, 10 is YYYY-MM-DD - dt = dateutil.parser.parse(datestr) + dt = arrow.get(datestr).datetime resolution = DateTimeResolution.date elif len(datestr) > 10: - dt = dateutil.parser.parse(datestr) + dt = arrow.get(datestr).datetime resolution = DateTimeResolution.datetime return cls(dt, resolution) if dt else None diff --git a/pyproject.toml b/pyproject.toml index f3f8d8c5..94c31799 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,8 +24,8 @@ classifiers = [ "Topic :: Scientific/Engineering :: GIS", ] dependencies = [ + "arrow", "pygeoif>=1.1", - "python-dateutil", "typing-extensions>4", ] description = "Fast KML processing in python" diff --git a/requirements.txt b/requirements.txt index c7b6a580..d21c1195 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ +arrow # Base package requirements pygeoif>=1.1.0 -python-dateutil typing_extensions diff --git a/test-requirements.txt b/test-requirements.txt index 48485321..841375fc 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -15,6 +15,7 @@ pre-commit pytest pytest-cov pytest-randomly +python-dateutil radon types-python-dateutil types-setuptools diff --git a/tests/times_test.py b/tests/times_test.py index df3586d7..c276308b 100644 --- a/tests/times_test.py +++ b/tests/times_test.py @@ -98,7 +98,7 @@ def test_parse_year(self) -> None: dt = KmlDateTime.parse("2000") assert dt.resolution == DateTimeResolution.year - assert dt.dt == datetime.datetime(2000, 1, 1) + assert dt.dt == datetime.datetime(2000, 1, 1, tzinfo=tzutc()) def test_parse_year_0(self) -> None: with pytest.raises(ValueError): @@ -108,13 +108,13 @@ def test_parse_year_month(self) -> None: dt = KmlDateTime.parse("2000-03") assert dt.resolution == DateTimeResolution.year_month - assert dt.dt == datetime.datetime(2000, 3, 1) + assert dt.dt == datetime.datetime(2000, 3, 1, tzinfo=tzutc()) def test_parse_year_month_no_dash(self) -> None: dt = KmlDateTime.parse("200004") assert dt.resolution == DateTimeResolution.year_month - assert dt.dt == datetime.datetime(2000, 4, 1) + assert dt.dt == datetime.datetime(2000, 4, 1, tzinfo=tzutc()) def test_parse_year_month_0(self) -> None: with pytest.raises(ValueError): @@ -128,13 +128,13 @@ def test_parse_year_month_day(self) -> None: dt = KmlDateTime.parse("2000-03-01") assert dt.resolution == DateTimeResolution.date - assert dt.dt == datetime.datetime(2000, 3, 1) + assert dt.dt == datetime.datetime(2000, 3, 1, tzinfo=tzutc()) def test_parse_year_month_day_no_dash(self) -> None: dt = KmlDateTime.parse("20000401") assert dt.resolution == DateTimeResolution.date - assert dt.dt == datetime.datetime(2000, 4, 1) + assert dt.dt == datetime.datetime(2000, 4, 1, tzinfo=tzutc()) def test_parse_year_month_day_0(self) -> None: with pytest.raises(ValueError): @@ -166,7 +166,7 @@ def test_parse_datetime_no_tz(self) -> None: dt = KmlDateTime.parse("1997-07-16T07:30:15") assert dt.resolution == DateTimeResolution.datetime - assert dt.dt == datetime.datetime(1997, 7, 16, 7, 30, 15) + assert dt.dt == datetime.datetime(1997, 7, 16, 7, 30, 15, tzinfo=tzutc()) def test_parse_datetime_empty(self) -> None: assert KmlDateTime.parse("") is None @@ -294,7 +294,7 @@ def test_read_timestamp(self) -> None: ts.from_string(doc) assert ts.timestamp.resolution == DateTimeResolution.year - assert ts.timestamp.dt == datetime.datetime(1997, 1, 1, 0, 0) + assert ts.timestamp.dt == datetime.datetime(1997, 1, 1, 0, 0, tzinfo=tzutc()) doc = """ 1997-07 @@ -303,7 +303,7 @@ def test_read_timestamp(self) -> None: ts.from_string(doc) assert ts.timestamp.resolution == DateTimeResolution.year_month - assert ts.timestamp.dt == datetime.datetime(1997, 7, 1, 0, 0) + assert ts.timestamp.dt == datetime.datetime(1997, 7, 1, 0, 0, tzinfo=tzutc()) doc = """ 199808 @@ -312,7 +312,7 @@ def test_read_timestamp(self) -> None: ts.from_string(doc) assert ts.timestamp.resolution == DateTimeResolution.year_month - assert ts.timestamp.dt == datetime.datetime(1998, 8, 1, 0, 0) + assert ts.timestamp.dt == datetime.datetime(1998, 8, 1, 0, 0, tzinfo=tzutc()) doc = """ 1997-07-16 @@ -321,7 +321,7 @@ def test_read_timestamp(self) -> None: ts.from_string(doc) assert ts.timestamp.resolution == DateTimeResolution.date - assert ts.timestamp.dt == datetime.datetime(1997, 7, 16, 0, 0) + assert ts.timestamp.dt == datetime.datetime(1997, 7, 16, 0, 0, tzinfo=tzutc()) # dateTime (YYYY-MM-DDThh:mm:ssZ) # Here, T is the separator between the calendar and the hourly notation # of time, and Z indicates UTC. (Seconds are required.) @@ -359,7 +359,7 @@ def test_read_timespan(self) -> None: ts.from_string(doc) assert ts.begin.resolution == DateTimeResolution.date - assert ts.begin.dt == datetime.datetime(1876, 8, 1, 0, 0) + assert ts.begin.dt == datetime.datetime(1876, 8, 1, 0, 0, tzinfo=tzutc()) assert ts.end.resolution == DateTimeResolution.datetime assert ts.end.dt == datetime.datetime(1997, 7, 16, 7, 30, 15, tzinfo=tzutc()) diff --git a/tests/views_test.py b/tests/views_test.py index 3b886a72..136e9980 100644 --- a/tests/views_test.py +++ b/tests/views_test.py @@ -17,7 +17,7 @@ """Test the (Abstract)Views classes.""" import datetime - +from dateutil.tz import tzutc from fastkml import times from fastkml import views from tests.base import Lxml @@ -31,8 +31,8 @@ def test_create_camera(self) -> None: """Test the creation of a camera.""" time_span = times.TimeSpan( id="time-span-id", - begin=times.KmlDateTime(datetime.datetime(2019, 1, 1)), - end=times.KmlDateTime(datetime.datetime(2019, 1, 2)), + begin=times.KmlDateTime(datetime.datetime(2019, 1, 1, tzinfo=tzutc())), + end=times.KmlDateTime(datetime.datetime(2019, 1, 2, tzinfo=tzutc())), ) camera = views.Camera( @@ -57,8 +57,8 @@ def test_create_camera(self) -> None: assert camera.longitude == 60 assert camera.id == "cam-id" assert camera.target_id == "target-cam-id" - assert camera.begin == times.KmlDateTime(datetime.datetime(2019, 1, 1)) - assert camera.end == times.KmlDateTime(datetime.datetime(2019, 1, 2)) + assert camera.begin == times.KmlDateTime(datetime.datetime(2019, 1, 1, tzinfo=tzutc())) + assert camera.end == times.KmlDateTime(datetime.datetime(2019, 1, 2, tzinfo=tzutc())) assert camera.to_string() def test_camera_read(self) -> None: @@ -92,13 +92,13 @@ def test_camera_read(self) -> None: assert camera.longitude == 60 assert camera.id == "cam-id" assert camera.target_id == "target-cam-id" - assert camera.begin == times.KmlDateTime(datetime.datetime(2019, 1, 1)) - assert camera.end == times.KmlDateTime(datetime.datetime(2019, 1, 2)) + assert camera.begin == times.KmlDateTime(datetime.datetime(2019, 1, 1, tzinfo=tzutc())) + assert camera.end == times.KmlDateTime(datetime.datetime(2019, 1, 2, tzinfo=tzutc())) def test_create_look_at(self) -> None: time_stamp = times.TimeStamp( id="time-span-id", - timestamp=times.KmlDateTime(datetime.datetime(2019, 1, 1)), + timestamp=times.KmlDateTime(datetime.datetime(2019, 1, 1, tzinfo=tzutc())), ) look_at = views.LookAt( @@ -121,7 +121,7 @@ def test_create_look_at(self) -> None: assert look_at.longitude == 60 assert look_at.id == "look-at-id" assert look_at.target_id == "target-look-at-id" - assert look_at._timestamp.timestamp.dt == datetime.datetime(2019, 1, 1) + assert look_at._timestamp.timestamp.dt == datetime.datetime(2019, 1, 1, tzinfo=tzutc()) assert look_at.begin is None assert look_at.end is None assert look_at.to_string() @@ -153,7 +153,7 @@ def test_look_at_read(self) -> None: assert look_at.longitude == 60 assert look_at.id == "look-at-id" assert look_at.target_id == "target-look-at-id" - assert look_at._timestamp.timestamp.dt == datetime.datetime(2019, 1, 1) + assert look_at._timestamp.timestamp.dt == datetime.datetime(2019, 1, 1, tzinfo=tzutc()) assert look_at.begin is None assert look_at.end is None From 029efde0e1d3e4c4fbe4bc570932d42e7aabf502 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 15:31:30 +0000 Subject: [PATCH 22/63] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- fastkml/times.py | 1 - tests/views_test.py | 26 ++++++++++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/fastkml/times.py b/fastkml/times.py index 707a5589..97172ace 100644 --- a/fastkml/times.py +++ b/fastkml/times.py @@ -21,7 +21,6 @@ from typing import Union import arrow -from dateutil import parser import fastkml.config as config from fastkml.base import _BaseObject diff --git a/tests/views_test.py b/tests/views_test.py index 136e9980..5f1f23b2 100644 --- a/tests/views_test.py +++ b/tests/views_test.py @@ -17,7 +17,9 @@ """Test the (Abstract)Views classes.""" import datetime + from dateutil.tz import tzutc + from fastkml import times from fastkml import views from tests.base import Lxml @@ -57,8 +59,12 @@ def test_create_camera(self) -> None: assert camera.longitude == 60 assert camera.id == "cam-id" assert camera.target_id == "target-cam-id" - assert camera.begin == times.KmlDateTime(datetime.datetime(2019, 1, 1, tzinfo=tzutc())) - assert camera.end == times.KmlDateTime(datetime.datetime(2019, 1, 2, tzinfo=tzutc())) + assert camera.begin == times.KmlDateTime( + datetime.datetime(2019, 1, 1, tzinfo=tzutc()) + ) + assert camera.end == times.KmlDateTime( + datetime.datetime(2019, 1, 2, tzinfo=tzutc()) + ) assert camera.to_string() def test_camera_read(self) -> None: @@ -92,8 +98,12 @@ def test_camera_read(self) -> None: assert camera.longitude == 60 assert camera.id == "cam-id" assert camera.target_id == "target-cam-id" - assert camera.begin == times.KmlDateTime(datetime.datetime(2019, 1, 1, tzinfo=tzutc())) - assert camera.end == times.KmlDateTime(datetime.datetime(2019, 1, 2, tzinfo=tzutc())) + assert camera.begin == times.KmlDateTime( + datetime.datetime(2019, 1, 1, tzinfo=tzutc()) + ) + assert camera.end == times.KmlDateTime( + datetime.datetime(2019, 1, 2, tzinfo=tzutc()) + ) def test_create_look_at(self) -> None: time_stamp = times.TimeStamp( @@ -121,7 +131,9 @@ def test_create_look_at(self) -> None: assert look_at.longitude == 60 assert look_at.id == "look-at-id" assert look_at.target_id == "target-look-at-id" - assert look_at._timestamp.timestamp.dt == datetime.datetime(2019, 1, 1, tzinfo=tzutc()) + assert look_at._timestamp.timestamp.dt == datetime.datetime( + 2019, 1, 1, tzinfo=tzutc() + ) assert look_at.begin is None assert look_at.end is None assert look_at.to_string() @@ -153,7 +165,9 @@ def test_look_at_read(self) -> None: assert look_at.longitude == 60 assert look_at.id == "look-at-id" assert look_at.target_id == "target-look-at-id" - assert look_at._timestamp.timestamp.dt == datetime.datetime(2019, 1, 1, tzinfo=tzutc()) + assert look_at._timestamp.timestamp.dt == datetime.datetime( + 2019, 1, 1, tzinfo=tzutc() + ) assert look_at.begin is None assert look_at.end is None From e6e73ff9d7b53d50a0f8653f2f69b694cc237f48 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 16:35:38 +0000 Subject: [PATCH 23/63] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.10.0 → 23.10.1](https://github.com/psf/black/compare/23.10.0...23.10.1) - [github.com/astral-sh/ruff-pre-commit: v0.1.1 → v0.1.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.1...v0.1.3) --- .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 dc6c5acc..b7ef9531 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,7 +47,7 @@ repos: - id: pyupgrade args: ["--py3-plus", "--py37-plus"] - repo: https://github.com/psf/black - rev: 23.10.0 + rev: 23.10.1 hooks: - id: black - repo: https://github.com/PyCQA/flake8 @@ -59,7 +59,7 @@ repos: hooks: - id: isort - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.1.1' + rev: 'v0.1.3' hooks: - id: ruff - repo: https://github.com/PyCQA/flake8 From abd555da40417bc64b2e9f26d326519012426825 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Mon, 30 Oct 2023 17:13:25 +0000 Subject: [PATCH 24/63] start refactor of data.py #254 --- .sourcery.yaml | 2 +- fastkml/data.py | 166 ++++++++++++++---------------------------- fastkml/enums.py | 25 +++++++ fastkml/exceptions.py | 3 +- tests/data_test.py | 87 ++++++++++++++++------ tests/oldunit_test.py | 49 ------------- 6 files changed, 147 insertions(+), 185 deletions(-) diff --git a/.sourcery.yaml b/.sourcery.yaml index f300aba4..35fa4dc6 100644 --- a/.sourcery.yaml +++ b/.sourcery.yaml @@ -1,2 +1,2 @@ refactor: - python_version: '3.7' + python_version: '3.8' diff --git a/fastkml/data.py b/fastkml/data.py index 49d8169b..cd1958a1 100644 --- a/fastkml/data.py +++ b/fastkml/data.py @@ -15,19 +15,21 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA """Add Custom Data""" import logging +from dataclasses import dataclass from typing import Dict +from typing import Iterable from typing import List from typing import Optional from typing import Tuple from typing import Union from typing import overload -from typing_extensions import TypedDict - import fastkml.config as config from fastkml.base import _BaseObject from fastkml.base import _XMLObject +from fastkml.enums import DataType from fastkml.enums import Verbosity +from fastkml.exceptions import KMLSchemaError from fastkml.types import Element __all__ = [ @@ -42,31 +44,38 @@ "SchemaDataTupleInput", "SchemaDataType", "SimpleField", - "SimpleFields", - "SimpleFieldsDictInput", - "SimpleFieldsInput", - "SimpleFieldsListInput", - "SimpleFieldsOutput", - "SimpleFieldsTupleInput", ] logger = logging.getLogger(__name__) -class SimpleField(TypedDict): - name: str - type: str - displayName: Optional[str] # noqa: N815 - +@dataclass(frozen=True) +class SimpleField: + """ + A SimpleField always has both name and type attributes. + + The declaration of the custom field, must specify both the type + and the name of this field. + If either the type or the name is omitted, the field is ignored. + + The type can be one of the following: + - string + - int + - uint + - short + - ushort + - float + - double + - bool + + The displayName, if any, to be used when the field name is displayed to + the Google Earth user. Use the [CDATA] element to escape standard + HTML markup. + """ -SimpleFields = List[Dict[str, str]] -SimpleFieldsListInput = List[Union[Dict[str, str], List[Dict[str, str]]]] -SimpleFieldsTupleInput = Tuple[Union[Dict[str, str], Tuple[Dict[str, str]]]] -SimpleFieldsDictInput = Dict[str, str] -SimpleFieldsInput = Optional[ - Union[SimpleFieldsListInput, SimpleFieldsTupleInput, SimpleFieldsDictInput] -] -SimpleFieldsOutput = Tuple[SimpleField, ...] + name: str + type: DataType + display_name: Optional[str] = None class Schema(_BaseObject): @@ -85,101 +94,36 @@ def __init__( id: Optional[str] = None, target_id: Optional[str] = None, name: Optional[str] = None, - fields: SimpleFieldsInput = None, + fields: Optional[Iterable[SimpleField]] = None, ) -> None: if id is None: - raise ValueError("Id is required for schema") + raise KMLSchemaError("Id is required for schema") super().__init__(ns=ns, id=id, target_id=target_id) self.name = name - self._simple_fields: SimpleFields = [] - self.simple_fields = fields # type: ignore[assignment] + self._simple_fields = list(fields) if fields else [] - @property - def simple_fields(self) -> SimpleFieldsOutput: - return tuple( - SimpleField( - type=simple_field.get("type", ""), - name=simple_field.get("name", ""), - displayName=simple_field.get("displayName") or None, - ) - for simple_field in self._simple_fields - if simple_field.get("type") and simple_field.get("name") + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}(" + f"ns={self.ns!r}, " + f"id={self.id!r}, " + f"target_id={self.target_id!r}, " + f"name={self.name}, " + f"fields={self.simple_fields!r}" + ")" ) - @simple_fields.setter - @overload - def simple_fields(self, fields: SimpleFieldsListInput) -> None: - ... - - @simple_fields.setter - @overload - def simple_fields(self, fields: SimpleFieldsTupleInput) -> None: - ... + @property + def simple_fields(self) -> Tuple[SimpleField, ...]: + return tuple(self._simple_fields) @simple_fields.setter - @overload - def simple_fields(self, fields: SimpleFieldsDictInput) -> None: - ... + def simple_fields(self, fields: Iterable[SimpleField]) -> None: + self._simple_fields = list(fields) - @simple_fields.setter - def simple_fields(self, fields: SimpleFieldsInput) -> None: - if isinstance(fields, dict): - self.append(**fields) - elif isinstance(fields, (list, tuple)): - for field in fields: - if isinstance(field, (list, tuple)): - self.append(*field) - elif isinstance(field, dict): - self.append(**field) - elif fields is None: - self._simple_fields = [] - else: - raise ValueError("Fields must be of type list, tuple or dict") - - def append(self, type: str, name: str, display_name: Optional[str] = None) -> None: - """ - append a field. - The declaration of the custom field, must specify both the type - and the name of this field. - If either the type or the name is omitted, the field is ignored. - - The type can be one of the following: - string - int - uint - short - ushort - float - double - bool - - - The name, if any, to be used when the field name is displayed to - the Google Earth user. Use the [CDATA] element to escape standard - HTML markup. - """ - allowed_types = [ - "string", - "int", - "uint", - "short", - "ushort", - "float", - "double", - "bool", - ] - if type not in allowed_types: - logger.warning( - "%s has the type %s which is invalid. " - "The type must be one of " - "'string', 'int', 'uint', 'short', " - "'ushort', 'float', 'double', 'bool'", - name, - type, - ) - self._simple_fields.append( - {"type": type, "name": name, "displayName": display_name or ""} - ) + def append(self, field: SimpleField) -> None: + """Append a field.""" + self._simple_fields.append(field) def from_element(self, element: Element) -> None: super().from_element(element) @@ -190,7 +134,7 @@ def from_element(self, element: Element) -> None: sftype = simple_field.get("type") display_name = simple_field.find(f"{self.ns}displayName") sfdisplay_name = display_name.text if display_name is not None else None - self.append(sftype, sfname, sfdisplay_name) + self.append(SimpleField(sfname, DataType(sftype), sfdisplay_name)) def etree_element( self, @@ -204,13 +148,13 @@ def etree_element( sf = config.etree.SubElement( # type: ignore[attr-defined] element, f"{self.ns}SimpleField" ) - sf.set("type", simple_field["type"]) - sf.set("name", simple_field["name"]) - if simple_field.get("displayName"): + sf.set("type", simple_field.type.value) + sf.set("name", simple_field.name) + if simple_field.display_name: dn = config.etree.SubElement( # type: ignore[attr-defined] sf, f"{self.ns}displayName" ) - dn.text = simple_field["displayName"] + dn.text = simple_field.display_name return element diff --git a/fastkml/enums.py b/fastkml/enums.py index 25d1d802..431d7660 100644 --- a/fastkml/enums.py +++ b/fastkml/enums.py @@ -15,6 +15,7 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from enum import Enum from enum import unique +from typing import Union __all__ = ["AltitudeMode", "DateTimeResolution", "Verbosity"] @@ -84,3 +85,27 @@ class AltitudeMode(Enum): absolute = "absolute" clamp_to_sea_floor = "clampToSeaFloor" relative_to_sea_floor = "relativeToSeaFloor" + + +@unique +class DataType(Enum): + string = "string" + int_ = "int" + uint = "uint" + short = "short" + ushort = "ushort" + float_ = "float" + double = "double" + bool_ = "bool" + + def convert(self, value: str) -> Union[str, int, float, bool]: + """Convert the data type to a python type.""" + if self == DataType.string: + return str(value) + if self in {DataType.int_, DataType.uint, DataType.short, DataType.ushort}: + return int(value) + if self in {DataType.float_, DataType.double}: + return float(value) + if self == DataType.bool_: + return bool(int(value)) + raise ValueError(f"Unknown data type {self}") diff --git a/fastkml/exceptions.py b/fastkml/exceptions.py index 51e1bcde..56226835 100644 --- a/fastkml/exceptions.py +++ b/fastkml/exceptions.py @@ -28,4 +28,5 @@ class KMLWriteError(FastKMLError): """Raised when there is an error writing KML.""" -__all__ = ["FastKMLError", "KMLParseError", "KMLWriteError"] +class KMLSchemaError(FastKMLError): + """Raised when there is an error with the KML Schema.""" diff --git a/tests/data_test.py b/tests/data_test.py index adad5f7b..779be6c1 100644 --- a/tests/data_test.py +++ b/tests/data_test.py @@ -17,7 +17,10 @@ import pytest import fastkml as kml +from fastkml import config from fastkml import data +from fastkml.enums import DataType +from fastkml.exceptions import KMLSchemaError from tests.base import Lxml from tests.base import StdLibrary @@ -25,34 +28,72 @@ class TestStdLibrary(StdLibrary): """Test with the standard library.""" + def test_schema_requires_id(self) -> None: + pytest.raises(KMLSchemaError, kml.Schema, "") + def test_schema(self) -> None: ns = "{http://www.opengis.net/kml/2.2}" # noqa: FS003 - pytest.raises(ValueError, kml.Schema, ns) s = kml.Schema(ns, "some_id") assert not list(s.simple_fields) - s.append("int", "Integer", "An Integer") - assert list(s.simple_fields)[0]["type"] == "int" - assert list(s.simple_fields)[0]["name"] == "Integer" - assert list(s.simple_fields)[0]["displayName"] == "An Integer" - s.simple_fields = None - assert not list(s.simple_fields) - pytest.raises(TypeError, s.append, ("none", "Integer", "An Integer")) - # pytest.raises( - # TypeError, s.simple_fields, ("none", "Integer", "An Integer"), - # ) - # pytest.raises(TypeError, s.simple_fields, ("int", "Integer", "An Integer")) + field = data.SimpleField( + name="Integer", type=DataType.int_, display_name="An Integer" + ) + s.append(field) + assert s.simple_fields[0] == field + s.simple_fields = [] + assert not s.simple_fields fields = {"type": "int", "name": "Integer", "display_name": "An Integer"} - s.simple_fields = fields - assert list(s.simple_fields)[0]["type"] == "int" - assert list(s.simple_fields)[0]["name"] == "Integer" - assert list(s.simple_fields)[0]["displayName"] == "An Integer" - s.simple_fields = [["float", "Float"], fields] - assert list(s.simple_fields)[0]["type"] == "int" - assert list(s.simple_fields)[0]["name"] == "Integer" - assert list(s.simple_fields)[0]["displayName"] == "An Integer" - assert list(s.simple_fields)[1]["type"] == "float" - assert list(s.simple_fields)[1]["name"] == "Float" - assert list(s.simple_fields)[1]["displayName"] is None + s.simple_fields = (data.SimpleField(**fields),) + assert s.simple_fields[0] == data.SimpleField(**fields) + + def test_schema_from_string(self) -> None: + doc = """ + + Trail Head Name]]> + + + The length in miles]]> + + + change in altitude]]> + + """ + + s = kml.Schema(ns="", id="default") + s.from_string(doc) + assert len(list(s.simple_fields)) == 3 + assert s.simple_fields[0].type == DataType("string") + assert s.simple_fields[1].type == DataType("double") + assert s.simple_fields[2].type == DataType("int") + assert s.simple_fields[0].name == "TrailHeadName" + assert s.simple_fields[1].name == "TrailLength" + assert s.simple_fields[2].name == "ElevationGain" + assert s.simple_fields[0].display_name == "Trail Head Name" + assert s.simple_fields[1].display_name == "The length in miles" + assert s.simple_fields[2].display_name == "change in altitude" + s1 = kml.Schema(ns="", id="default") + s1.from_string(s.to_string()) + assert len(s1.simple_fields) == 3 + assert s1.simple_fields[0].type == DataType("string") + assert s1.simple_fields[1].name == "TrailLength" + assert s1.simple_fields[2].display_name == "change in altitude" + assert s.to_string() == s1.to_string() + doc1 = f""" + + {doc} + + """ + k = kml.KML() + k.from_string(doc1) + d = list(k.features())[0] + s2 = list(d.schemata())[0] + s.ns = config.KMLNS + assert s.to_string() == s2.to_string() + k1 = kml.KML() + k1.from_string(k.to_string()) + assert "Schema" in k1.to_string() + assert "SimpleField" in k1.to_string() + assert k1.to_string() == k.to_string() def test_schema_data(self) -> None: ns = "{http://www.opengis.net/kml/2.2}" # noqa: FS003 diff --git a/tests/oldunit_test.py b/tests/oldunit_test.py index ee4f2c9d..db7f017c 100644 --- a/tests/oldunit_test.py +++ b/tests/oldunit_test.py @@ -542,55 +542,6 @@ def test_multipoints(self) -> None: def test_atom(self) -> None: pass - def test_schema(self) -> None: - doc = """ - - Trail Head Name]]> - - - The length in miles]]> - - - change in altitude]]> - - """ - - s = kml.Schema(ns="", id="default") - s.from_string(doc) - assert len(list(s.simple_fields)) == 3 - assert list(s.simple_fields)[0]["type"] == "string" - assert list(s.simple_fields)[1]["type"] == "double" - assert list(s.simple_fields)[2]["type"] == "int" - assert list(s.simple_fields)[0]["name"] == "TrailHeadName" - assert list(s.simple_fields)[1]["name"] == "TrailLength" - assert list(s.simple_fields)[2]["name"] == "ElevationGain" - assert list(s.simple_fields)[0]["displayName"] == "Trail Head Name" - assert list(s.simple_fields)[1]["displayName"] == "The length in miles" - assert list(s.simple_fields)[2]["displayName"] == "change in altitude" - s1 = kml.Schema(ns="", id="default") - s1.from_string(s.to_string()) - assert len(list(s1.simple_fields)) == 3 - assert list(s1.simple_fields)[0]["type"] == "string" - assert list(s1.simple_fields)[1]["name"] == "TrailLength" - assert list(s1.simple_fields)[2]["displayName"] == "change in altitude" - assert s.to_string() == s1.to_string() - doc1 = f""" - - {doc} - - """ - k = kml.KML() - k.from_string(doc1) - d = list(k.features())[0] - s2 = list(d.schemata())[0] - s.ns = config.KMLNS - assert s.to_string() == s2.to_string() - k1 = kml.KML() - k1.from_string(k.to_string()) - assert "Schema" in k1.to_string() - assert "SimpleField" in k1.to_string() - assert k1.to_string() == k.to_string() - def test_snippet(self) -> None: doc = """ From 1623c7477d055adade4d3d3e57865dbd4ecfb20c Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Mon, 30 Oct 2023 18:09:33 +0000 Subject: [PATCH 25/63] parse whole date with regex --- fastkml/data.py | 8 ++++---- fastkml/times.py | 27 +++++++++++++-------------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/fastkml/data.py b/fastkml/data.py index cd1958a1..f36442db 100644 --- a/fastkml/data.py +++ b/fastkml/data.py @@ -130,11 +130,11 @@ def from_element(self, element: Element) -> None: self.name = element.get("name") simple_fields = element.findall(f"{self.ns}SimpleField") for simple_field in simple_fields: - sfname = simple_field.get("name") - sftype = simple_field.get("type") + sf_name = simple_field.get("name") + sf_type = simple_field.get("type") display_name = simple_field.find(f"{self.ns}displayName") - sfdisplay_name = display_name.text if display_name is not None else None - self.append(SimpleField(sfname, DataType(sftype), sfdisplay_name)) + sf_display_name = display_name.text if display_name is not None else None + self.append(SimpleField(sf_name, DataType(sf_type), sf_display_name)) def etree_element( self, diff --git a/fastkml/times.py b/fastkml/times.py index 97172ace..980e157f 100644 --- a/fastkml/times.py +++ b/fastkml/times.py @@ -31,7 +31,9 @@ # regular expression to parse a gYearMonth string # year and month may be separated by a dash or not # year is always 4 digits, month is always 2 digits -year_month = re.compile(r"^(?P\d{4})(?:-?)(?P\d{2})$") +year_month_day = re.compile( + r"^(?P\d{4})(?:-)?(?P\d{2})?(?:-)?(?P\d{2})?$" +) class KmlDateTime: @@ -120,20 +122,17 @@ def parse(cls, datestr: str) -> Optional["KmlDateTime"]: """Parse a KML DateTime string into a KmlDateTime object.""" resolution = None dt = None - if len(datestr) == 4: - year = int(datestr) - dt = arrow.get(year, 1, 1).datetime - resolution = DateTimeResolution.year - elif len(datestr) in {6, 7}: - ym = year_month.match(datestr) - if ym: - year = int(ym.group("year")) - month = int(ym.group("month")) - dt = arrow.get(year, month, 1).datetime - resolution = DateTimeResolution.year_month - elif len(datestr) in {8, 10}: # 8 is YYYYMMDD, 10 is YYYY-MM-DD - dt = arrow.get(datestr).datetime + year_month_day_match = year_month_day.match(datestr) + if year_month_day_match: + year = int(year_month_day_match.group("year")) + month = int(year_month_day_match.group("month") or 1) + day = int(year_month_day_match.group("day") or 1) + dt = arrow.get(year, month, day).datetime resolution = DateTimeResolution.date + if year_month_day_match.group("day") is None: + resolution = DateTimeResolution.year_month + if year_month_day_match.group("month") is None: + resolution = DateTimeResolution.year elif len(datestr) > 10: dt = arrow.get(datestr).datetime resolution = DateTimeResolution.datetime From c5d67fe73a48542b0686dc374891276d9e25237d Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Mon, 30 Oct 2023 19:00:16 +0000 Subject: [PATCH 26/63] refactor Schema --- fastkml/data.py | 49 ++++++++++++++++++++++++++++++++++++++++++++-- tests/data_test.py | 5 ++--- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/fastkml/data.py b/fastkml/data.py index f36442db..f27e7d03 100644 --- a/fastkml/data.py +++ b/fastkml/data.py @@ -16,12 +16,14 @@ """Add Custom Data""" import logging from dataclasses import dataclass +from typing import Any from typing import Dict from typing import Iterable from typing import List from typing import Optional from typing import Tuple from typing import Union +from typing import cast from typing import overload import fastkml.config as config @@ -80,10 +82,13 @@ class SimpleField: class Schema(_BaseObject): """ - Specifies a custom KML schema that is used to add custom data to - KML Features. + Specifies a custom KML schema that is used to add custom data to KML Features. + The "id" attribute is required and must be unique within the KML file. is always a child of . + + https://developers.google.com/kml/documentation/extendeddata#declare-the-schema-element + """ __name__ = "Schema" @@ -157,6 +162,46 @@ def etree_element( dn.text = simple_field.display_name return element + @classmethod + def _get_fields_kwargs_from_element( + cls, + *, + ns: str, + element: Element, + strict: bool, + ) -> List[SimpleField]: + def get_display_name(field: Element) -> Optional[str]: + display_name = field.find(f"{ns}displayName") + return display_name.text if display_name is not None else None + + return [ + cast( + SimpleField, + SimpleField( + name=field.get("name"), + type=DataType(field.get("type")), + display_name=get_display_name(field), + ), + ) + for field in element.findall(f"{ns}SimpleField") + if field is not None + ] + + @classmethod + def _get_kwargs( + cls, + *, + ns: str, + element: Element, + strict: bool, + ) -> Dict[str, Any]: + kwargs = super()._get_kwargs(ns=ns, element=element, strict=strict) + kwargs["name"] = element.get("name") + kwargs["fields"] = cls._get_fields_kwargs_from_element( + ns=ns, element=element, strict=strict + ) + return kwargs + class Data(_XMLObject): """Represents an untyped name/value pair with optional display name.""" diff --git a/tests/data_test.py b/tests/data_test.py index 779be6c1..274d5707 100644 --- a/tests/data_test.py +++ b/tests/data_test.py @@ -59,9 +59,8 @@ def test_schema_from_string(self) -> None: """ - s = kml.Schema(ns="", id="default") - s.from_string(doc) - assert len(list(s.simple_fields)) == 3 + s = kml.Schema.class_from_string(doc, ns="") + assert len(s.simple_fields) == 3 assert s.simple_fields[0].type == DataType("string") assert s.simple_fields[1].type == DataType("double") assert s.simple_fields[2].type == DataType("int") From 987ea998d2d40d6d451918e0806cbaef9e7a4e1f Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Mon, 30 Oct 2023 19:12:26 +0000 Subject: [PATCH 27/63] use class_from_element to create Schemas --- fastkml/kml.py | 5 ++--- tests/data_test.py | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/fastkml/kml.py b/fastkml/kml.py index 848f1614..22adc28f 100644 --- a/fastkml/kml.py +++ b/fastkml/kml.py @@ -1576,7 +1576,7 @@ def append_schema(self, schema: "Schema") -> None: s = Schema(schema) self._schemata.append(s) - def from_element(self, element: Element) -> None: + def from_element(self, element: Element, strict: bool = False) -> None: super().from_element(element) documents = element.findall(f"{self.ns}Document") for document in documents: @@ -1595,8 +1595,7 @@ def from_element(self, element: Element) -> None: self.append(feature) schemata = element.findall(f"{self.ns}Schema") for schema in schemata: - s = Schema(self.ns, id="default") - s.from_element(schema) + s = Schema.class_from_element(ns=self.ns, element=schema, strict=strict) self.append_schema(s) def etree_element( diff --git a/tests/data_test.py b/tests/data_test.py index 274d5707..dde02d77 100644 --- a/tests/data_test.py +++ b/tests/data_test.py @@ -70,8 +70,7 @@ def test_schema_from_string(self) -> None: assert s.simple_fields[0].display_name == "Trail Head Name" assert s.simple_fields[1].display_name == "The length in miles" assert s.simple_fields[2].display_name == "change in altitude" - s1 = kml.Schema(ns="", id="default") - s1.from_string(s.to_string()) + s1 = kml.Schema.class_from_string(s.to_string(), ns="") assert len(s1.simple_fields) == 3 assert s1.simple_fields[0].type == DataType("string") assert s1.simple_fields[1].name == "TrailLength" From b9078eb4b0b7d8c4e86b8a61eb9e665293310748 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Mon, 30 Oct 2023 19:25:51 +0000 Subject: [PATCH 28/63] refactor class Data --- fastkml/data.py | 18 ++++++++++++++++++ tests/data_test.py | 16 ++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/fastkml/data.py b/fastkml/data.py index f27e7d03..acae0ded 100644 --- a/fastkml/data.py +++ b/fastkml/data.py @@ -257,6 +257,24 @@ def from_element(self, element: Element) -> None: if display_name is not None: self.display_name = display_name.text + @classmethod + def _get_kwargs( + cls, + *, + ns: str, + element: Element, + strict: bool, + ) -> Dict[str, Any]: + kwargs = super()._get_kwargs(ns=ns, element=element, strict=strict) + kwargs["name"] = element.get("name") + value = element.find(f"{ns}value") + if value is not None: + kwargs["value"] = value.text + display_name = element.find(f"{ns}displayName") + if display_name is not None: + kwargs["display_name"] = display_name.text + return kwargs + class ExtendedData(_XMLObject): """Represents a list of untyped name/value pairs. See docs: diff --git a/tests/data_test.py b/tests/data_test.py index dde02d77..2f399f03 100644 --- a/tests/data_test.py +++ b/tests/data_test.py @@ -237,6 +237,22 @@ def test_schema_data_from_str(self) -> None: assert sd1.schema_url == "#TrailHeadTypeId" assert sd.to_string() == sd1.to_string() + def test_data_from_string(self) -> None: + doc = """ + This is hole + ]]> + 1 + """ + + d = data.Data.class_from_string(doc, ns="") + assert d.name == "holeNumber" + assert d.value == "1" + assert "This is hole " in d.display_name + d1 = data.Data.class_from_string(d.to_string(), ns="") + assert d1.name == "holeNumber" + assert d.to_string() == d1.to_string() + class TestLxml(Lxml, TestStdLibrary): """Test with lxml.""" From 4c7588e2a606af808e6c6e2af1cb9803cf4be15e Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Mon, 30 Oct 2023 20:31:20 +0000 Subject: [PATCH 29/63] Refactor SchemaData --- fastkml/data.py | 88 ++++++++++++++++++++++++---------------------- tests/data_test.py | 41 ++++++++++++--------- 2 files changed, 71 insertions(+), 58 deletions(-) diff --git a/fastkml/data.py b/fastkml/data.py index acae0ded..38504c77 100644 --- a/fastkml/data.py +++ b/fastkml/data.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 Christian Ledermann +# Copyright (C) 2022 - 2023 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 @@ -13,7 +13,10 @@ # 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 -"""Add Custom Data""" +"""Add Custom Data + +https://developers.google.com/kml/documentation/extendeddata#example +""" import logging from dataclasses import dataclass from typing import Any @@ -24,7 +27,6 @@ from typing import Tuple from typing import Union from typing import cast -from typing import overload import fastkml.config as config from fastkml.base import _BaseObject @@ -333,6 +335,12 @@ def from_element(self, element: Element) -> None: SchemaDataOutput = Tuple[Dict[str, Union[int, str]], ...] +@dataclass(frozen=True) +class SimpleData: + name: str + value: Union[int, str, float, bool] + + class SchemaData(_XMLObject): """ @@ -352,53 +360,32 @@ def __init__( self, ns: Optional[str] = None, schema_url: Optional[str] = None, - data: Optional[List[Dict[str, str]]] = None, + data: Optional[Iterable[SimpleData]] = None, ) -> None: super().__init__(ns) if (not isinstance(schema_url, str)) or (not schema_url): raise ValueError("required parameter schema_url missing") self.schema_url = schema_url - self._data: SchemaDataType = [] - self.data = data # type: ignore[assignment] + self._data = list(data) if data else [] + + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}(" + f"ns='{self.ns}'," + f"schema_url='{self.schema_url}', " + f"data='{self.data}')" + ) @property - def data(self) -> SchemaDataOutput: + def data(self) -> Tuple[SimpleData, ...]: return tuple(self._data) @data.setter - @overload - def data(self, data: SchemaDataListInput) -> None: - ... - - @data.setter - @overload - def data(self, data: SchemaDataTupleInput) -> None: - ... - - @data.setter - @overload - def data(self, data: SchemaDataDictInput) -> None: - ... + def data(self, data: Iterable[SimpleData]) -> None: + self._data = list(data) - @data.setter - def data(self, data: SchemaDataInput) -> None: - if isinstance(data, (tuple, list)): - self._data = [] - for d in data: - if isinstance(d, (tuple, list)): - self.append_data(*d) - elif isinstance(d, dict): - self.append_data(**d) - elif data is None: - self._data = [] - else: - raise TypeError("data must be of type tuple or list") - - def append_data(self, name: str, value: Union[int, str]) -> None: - if isinstance(name, str) and name: - self._data.append({"name": name, "value": value}) - else: - raise TypeError("name must be a nonempty string") + def append_data(self, data: SimpleData) -> None: + self._data.append(data) def etree_element( self, @@ -411,8 +398,8 @@ def etree_element( sd = config.etree.SubElement( # type: ignore[attr-defined] element, f"{self.ns}SimpleData" ) - sd.set("name", data["name"]) - sd.text = data["value"] + sd.set("name", data.name) + sd.text = data.value return element def from_element(self, element: Element) -> None: @@ -421,4 +408,21 @@ def from_element(self, element: Element) -> None: self.schema_url = element.get("schemaUrl") simple_data = element.findall(f"{self.ns}SimpleData") for sd in simple_data: - self.append_data(sd.get("name"), sd.text) + self.append_data(SimpleData(name=sd.get("name"), value=sd.text)) + + @classmethod + def _get_kwargs( + cls, + *, + ns: str, + element: Element, + strict: bool, + ) -> Dict[str, Any]: + kwargs = super()._get_kwargs(ns=ns, element=element, strict=strict) + kwargs["schema_url"] = element.get("schemaUrl") + kwargs["data"] = [ + SimpleData(name=sd.get("name"), value=sd.text) + for sd in element.findall(f"{ns}SimpleData") + if sd is not None + ] + return kwargs diff --git a/tests/data_test.py b/tests/data_test.py index 2f399f03..3debd182 100644 --- a/tests/data_test.py +++ b/tests/data_test.py @@ -98,17 +98,22 @@ def test_schema_data(self) -> None: pytest.raises(ValueError, data.SchemaData, ns) pytest.raises(ValueError, data.SchemaData, ns, "") sd = data.SchemaData(ns, "#default") - sd.append_data("text", "Some Text") + sd.append_data(data.SimpleData("text", "Some Text")) assert len(sd.data) == 1 - sd.append_data(value=1, name="Integer") + sd.append_data(data.SimpleData(value=1, name="Integer")) assert len(sd.data) == 2 - assert sd.data[0] == {"value": "Some Text", "name": "text"} - assert sd.data[1] == {"value": 1, "name": "Integer"} - new_data = (("text", "Some new Text"), {"value": 2, "name": "Integer"}) + assert sd.data[0] == data.SimpleData(**{"value": "Some Text", "name": "text"}) + assert sd.data[1] == data.SimpleData(**{"value": 1, "name": "Integer"}) + new_data = ( + data.SimpleData("text", "Some new Text"), + data.SimpleData(**{"value": 2, "name": "Integer"}), + ) sd.data = new_data assert len(sd.data) == 2 - assert sd.data[0] == {"value": "Some new Text", "name": "text"} - assert sd.data[1] == {"value": 2, "name": "Integer"} + assert sd.data[0].name == "text" + assert sd.data[0].value == "Some new Text" + assert sd.data[1].name == "Integer" + assert sd.data[1].value == 2 def test_untyped_extended_data(self) -> None: ns = "{http://www.opengis.net/kml/2.2}" # noqa: FS003 @@ -216,8 +221,11 @@ def test_extended_data(self) -> None: "The par for this hole is " in extended_data.elements[1].display_name ) sd = extended_data.elements[2] - assert sd.data[0]["name"] == "TrailHeadName" - assert sd.data[1]["value"] == "347.45" + assert sd.data[0] == data.SimpleData( + name="TrailHeadName", value="Mount Everest" + ) + assert sd.data[1] == data.SimpleData(name="TrailLength", value="347.45") + assert sd.data[2] == data.SimpleData(name="ElevationGain", value="10000") def test_schema_data_from_str(self) -> None: doc = """ @@ -226,14 +234,15 @@ def test_schema_data_from_str(self) -> None: 10 """ - sd = data.SchemaData(ns="", schema_url="#default") - sd.from_string(doc) + sd = data.SchemaData.class_from_string(doc, ns="") assert sd.schema_url == "#TrailHeadTypeId" - assert sd.data[0] == {"name": "TrailHeadName", "value": "Pi in the sky"} - assert sd.data[1] == {"name": "TrailLength", "value": "3.14159"} - assert sd.data[2] == {"name": "ElevationGain", "value": "10"} - sd1 = data.SchemaData(ns="", schema_url="#default") - sd1.from_string(sd.to_string()) + assert sd.data[0].name == "TrailHeadName" + assert sd.data[0].value == "Pi in the sky" + assert sd.data[1].name == "TrailLength" + assert sd.data[1].value == "3.14159" + assert sd.data[2].name == "ElevationGain" + assert sd.data[2].value == "10" + sd1 = data.SchemaData.class_from_string(sd.to_string(), ns="") assert sd1.schema_url == "#TrailHeadTypeId" assert sd.to_string() == sd1.to_string() From b95b3c832753af323d0168720a400e6af9822759 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Mon, 30 Oct 2023 20:56:02 +0000 Subject: [PATCH 30/63] Refactor ExtendedData closes #245 --- fastkml/data.py | 163 +++++++++++++++++++----------------------------- fastkml/kml.py | 10 +-- 2 files changed, 69 insertions(+), 104 deletions(-) diff --git a/fastkml/data.py b/fastkml/data.py index 38504c77..ef94671d 100644 --- a/fastkml/data.py +++ b/fastkml/data.py @@ -26,7 +26,6 @@ from typing import Optional from typing import Tuple from typing import Union -from typing import cast import fastkml.config as config from fastkml.base import _BaseObject @@ -41,12 +40,6 @@ "ExtendedData", "Schema", "SchemaData", - "SchemaDataDictInput", - "SchemaDataInput", - "SchemaDataListInput", - "SchemaDataOutput", - "SchemaDataTupleInput", - "SchemaDataType", "SimpleField", ] @@ -132,17 +125,6 @@ def append(self, field: SimpleField) -> None: """Append a field.""" self._simple_fields.append(field) - def from_element(self, element: Element) -> None: - super().from_element(element) - self.name = element.get("name") - simple_fields = element.findall(f"{self.ns}SimpleField") - for simple_field in simple_fields: - sf_name = simple_field.get("name") - sf_type = simple_field.get("type") - display_name = simple_field.find(f"{self.ns}displayName") - sf_display_name = display_name.text if display_name is not None else None - self.append(SimpleField(sf_name, DataType(sf_type), sf_display_name)) - def etree_element( self, precision: Optional[int] = None, @@ -177,13 +159,10 @@ def get_display_name(field: Element) -> Optional[str]: return display_name.text if display_name is not None else None return [ - cast( - SimpleField, - SimpleField( - name=field.get("name"), - type=DataType(field.get("type")), - display_name=get_display_name(field), - ), + SimpleField( + name=field.get("name"), + type=DataType(field.get("type")), + display_name=get_display_name(field), ) for field in element.findall(f"{ns}SimpleField") if field is not None @@ -249,16 +228,6 @@ def etree_element( display_name.text = self.display_name return element - def from_element(self, element: Element) -> None: - super().from_element(element) - self.name = element.get("name") - tmp_value = element.find(f"{self.ns}value") - if tmp_value is not None: - self.value = tmp_value.text - display_name = element.find(f"{self.ns}displayName") - if display_name is not None: - self.display_name = display_name.text - @classmethod def _get_kwargs( cls, @@ -278,63 +247,6 @@ def _get_kwargs( return kwargs -class ExtendedData(_XMLObject): - """Represents a list of untyped name/value pairs. See docs: - - -> 'Adding Untyped Name/Value Pairs' - https://developers.google.com/kml/documentation/extendeddata - - """ - - __name__ = "ExtendedData" - - def __init__( - self, - ns: Optional[str] = None, - elements: Optional[List[Union[Data, "SchemaData"]]] = None, - ) -> None: - super().__init__(ns) - self.elements = elements or [] - - def etree_element( - self, - precision: Optional[int] = None, - verbosity: Verbosity = Verbosity.normal, - ) -> Element: - element = super().etree_element(precision=precision, verbosity=verbosity) - for subelement in self.elements: - element.append(subelement.etree_element()) - return element - - def from_element(self, element: Element) -> None: - super().from_element(element) - self.elements = [] - untyped_data = element.findall(f"{self.ns}Data") - for ud in untyped_data: - el_data = Data(self.ns) - el_data.from_element(ud) - self.elements.append(el_data) - typed_data = element.findall(f"{self.ns}SchemaData") - for sd in typed_data: - el_schema_data = SchemaData(self.ns, "dummy") - el_schema_data.from_element(sd) - self.elements.append(el_schema_data) - - -SchemaDataType = List[Dict[str, Union[int, str]]] -SchemaDataListInput = List[Union[Dict[str, str], SchemaDataType]] -SchemaDataTupleInput = Tuple[Union[Dict[str, str], Tuple[Dict[str, Union[int, str]]]]] -SchemaDataDictInput = Dict[str, Union[int, str]] -SchemaDataInput = Optional[ - Union[ - SchemaDataListInput, - SchemaDataTupleInput, - SchemaDataDictInput, - ] -] -SchemaDataOutput = Tuple[Dict[str, Union[int, str]], ...] - - @dataclass(frozen=True) class SimpleData: name: str @@ -402,14 +314,6 @@ def etree_element( sd.text = data.value return element - def from_element(self, element: Element) -> None: - super().from_element(element) - self.data = [] # type: ignore[assignment] - self.schema_url = element.get("schemaUrl") - simple_data = element.findall(f"{self.ns}SimpleData") - for sd in simple_data: - self.append_data(SimpleData(name=sd.get("name"), value=sd.text)) - @classmethod def _get_kwargs( cls, @@ -426,3 +330,62 @@ def _get_kwargs( if sd is not None ] return kwargs + + +class ExtendedData(_XMLObject): + """Represents a list of untyped name/value pairs. See docs: + + -> 'Adding Untyped Name/Value Pairs' + https://developers.google.com/kml/documentation/extendeddata + + """ + + __name__ = "ExtendedData" + + def __init__( + self, + ns: Optional[str] = None, + elements: Optional[Iterable[Union[Data, SchemaData]]] = None, + ) -> None: + super().__init__(ns) + self.elements = elements or [] + + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}(" + f"ns='{self.ns}'," + f"elements='{self.elements}')" + ) + + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + element = super().etree_element(precision=precision, verbosity=verbosity) + for subelement in self.elements: + element.append(subelement.etree_element()) + return element + + @classmethod + def _get_kwargs( + cls, + *, + ns: str, + element: Element, + strict: bool, + ) -> Dict[str, Any]: + kwargs = super()._get_kwargs(ns=ns, element=element, strict=strict) + elements = [] + untyped_data = element.findall(f"{ns}Data") + for ud in untyped_data: + el_data = Data.class_from_element(ns=ns, element=ud, strict=strict) + elements.append(el_data) + typed_data = element.findall(f"{ns}SchemaData") + for sd in typed_data: + el_schema_data = SchemaData.class_from_element( + ns=ns, element=sd, strict=strict + ) + elements.append(el_schema_data) + kwargs["elements"] = elements + return kwargs diff --git a/fastkml/kml.py b/fastkml/kml.py index 22adc28f..603ac48e 100644 --- a/fastkml/kml.py +++ b/fastkml/kml.py @@ -408,7 +408,7 @@ def etree_element( phone_number.text = self._phone_number return element - def from_element(self, element: Element) -> None: + def from_element(self, element: Element, strict: bool = False) -> None: super().from_element(element) name = element.find(f"{self.ns}name") if name is not None: @@ -465,9 +465,11 @@ def from_element(self, element: Element) -> None: self._atom_author = s extended_data = element.find(f"{self.ns}ExtendedData") if extended_data is not None: - x = ExtendedData(self.ns) - x.from_element(extended_data) - self.extended_data = x + self.extended_data = ExtendedData.class_from_element( + ns=self.ns, + element=extended_data, + strict=strict, + ) address = element.find(f"{self.ns}address") if address is not None: self.address = address.text From 0e8976b45a78ee4fe4f370ba38353dc24edc071d Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Wed, 1 Nov 2023 19:33:02 +0000 Subject: [PATCH 31/63] test crepr, add __repr__ to atom.py classes --- fastkml/atom.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/fastkml/atom.py b/fastkml/atom.py index 8a58d17d..44efe964 100644 --- a/fastkml/atom.py +++ b/fastkml/atom.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012 - 2021 Christian Ledermann +# Copyright (C) 2012 - 2023 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 @@ -164,6 +164,19 @@ def __init__( self.title = title self.length = length + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}(" + f"ns={self.ns!r}, " + f"href={self.href!r}, " + f"rel={self.rel!r}, " + f"type={self.type!r}, " + f"hreflang={self.hreflang!r}, " + f"title={self.title!r}, " + f"length={self.length!r}, " + ")" + ) + def from_element(self, element: Element) -> None: super().from_element(element) @@ -231,6 +244,16 @@ def __init__( self.uri = uri self.email = email + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}(" + f"ns={self.ns!r}, " + f"name={self.name!r}, " + f"uri={self.uri!r}, " + f"email={self.email!r}, " + ")" + ) + def etree_element( self, precision: Optional[int] = None, From f24bab5c3aade11b0ec691724fe3eab6c7adac5b Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Thu, 2 Nov 2023 09:18:16 +0000 Subject: [PATCH 32/63] update dependencies --- fastkml/config.py | 14 ++++---- pyproject.toml | 82 ++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 81 insertions(+), 15 deletions(-) diff --git a/fastkml/config.py b/fastkml/config.py index fd9dd08f..18938a86 100644 --- a/fastkml/config.py +++ b/fastkml/config.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012 - 2021 Christian Ledermann +# Copyright (C) 2012 - 2023 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 @@ -14,7 +14,7 @@ # along with this library; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -"""Frequently used constants and configuration options""" +"""Frequently used constants and configuration options.""" import logging import warnings from types import ModuleType @@ -33,7 +33,7 @@ from lxml import etree except ImportError: # pragma: no cover - warnings.warn("Package `lxml` missing. Pretty print will be disabled") + warnings.warn("Package `lxml` missing. Pretty print will be disabled") # noqa: B028 import xml.etree.ElementTree as etree # type: ignore[no-redef] # noqa: N813 @@ -42,13 +42,13 @@ def set_etree_implementation(implementation: ModuleType) -> None: """Set the etree implementation to use.""" - global etree + global etree # noqa: PLW0603 etree = implementation -KMLNS = "{http://www.opengis.net/kml/2.2}" # noqa: FS003 -ATOMNS = "{http://www.w3.org/2005/Atom}" # noqa: FS003 -GXNS = "{http://www.google.com/kml/ext/2.2}" # noqa: FS003 +KMLNS = "{http://www.opengis.net/kml/2.2}" +ATOMNS = "{http://www.w3.org/2005/Atom}" +GXNS = "{http://www.google.com/kml/ext/2.2}" NAME_SPACES = { "kml": KMLNS, diff --git a/pyproject.toml b/pyproject.toml index 9327753b..1ccf9e6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,6 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -33,7 +32,7 @@ dynamic = [ "version", ] name = "fastkml" -requires-python = ">=3.7" +requires-python = ">=3.8" [project.license] text = "LGPL" @@ -46,9 +45,11 @@ complexity = [ dev = [ "fastkml[complexity]", "fastkml[docs]", + "fastkml[lxml]", "fastkml[tests]", "fastkml[typing]", "pre-commit", + "shapely", ] docs = [ "Sphinx", @@ -79,7 +80,6 @@ tests = [ ] typing = [ "mypy", - "types-python-dateutil", ] [project.readme] @@ -140,8 +140,6 @@ warn_unused_ignores = true ignore_errors = true module = [ "fastkml.kml", - "fastkml.tests.config_test", - "fastkml.tests.oldunit_test", "fastkml.views", ] @@ -156,10 +154,78 @@ include = [ "fastkml", ] +[tool.ruff] +fix = true +ignore = [ + "ANN101", + "ANN102", + "D203", + "D212", + "FA100", +] +select = [ + "A", + "AIR", + "ANN", + "ARG", + "ASYNC", + "B", + "BLE", + "C4", + "C90", + "COM", + "CPY", + "D", + "DJ", + "DTZ", + "E", + "EM", + "ERA", + "EXE", + "F", + "FA", + "FBT", + "FIX", + "FLY", + "FURB", + "G", + "I", + "ICN", + "INP", + "INT", + "ISC", + "LOG", + "N", + "NPY", + "PD", + "PERF", + "PGH", + "PIE", + "PL", + "PT", + "PTH", + "PYI", + "Q", + "RET", + "RSE", + "RUF", + "S", + "SIM", + "SLF", + "SLOT", + "T10", + "T20", + "TCH", + "TD", + "TID", + "TRY", + "UP", + "W", + "YTT", +] +target-version = "py38" + [tool.ruff.extend-per-file-ignores] -"setup.py" = [ - "E501", -] "tests/oldunit_test.py" = [ "E501", ] From 7cc641040cf80957a21f47136b9797212fe58b9f Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Thu, 2 Nov 2023 09:29:18 +0000 Subject: [PATCH 33/63] update ruff isort config --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 1ccf9e6e..6c47d24d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -230,6 +230,9 @@ target-version = "py38" "E501", ] +[tool.ruff.isort] +force-single-line = true + [tool.setuptools] include-package-data = true zip-safe = false From 89a39186a0f9f3c85505c0cf68e5bfc305e316d5 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Thu, 2 Nov 2023 12:10:04 +0000 Subject: [PATCH 34/63] fix mypy error, update pyproject --- .github/workflows/run-all-tests.yml | 2 +- fastkml/config.py | 2 +- pyproject.toml | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index ef0e9b03..69cf74d2 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -56,7 +56,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.11'] + python-version: ['3.12'] steps: - uses: actions/checkout@v4 diff --git a/fastkml/config.py b/fastkml/config.py index 18938a86..6dd4a700 100644 --- a/fastkml/config.py +++ b/fastkml/config.py @@ -34,7 +34,7 @@ except ImportError: # pragma: no cover warnings.warn("Package `lxml` missing. Pretty print will be disabled") # noqa: B028 - import xml.etree.ElementTree as etree # type: ignore[no-redef] # noqa: N813 + import xml.etree.ElementTree as etree # noqa: N813 logger = logging.getLogger(__name__) diff --git a/pyproject.toml b/pyproject.toml index 6c47d24d..e57c7749 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,7 @@ complexity = [ dev = [ "fastkml[complexity]", "fastkml[docs]", + "fastkml[linting]", "fastkml[lxml]", "fastkml[tests]", "fastkml[typing]", From 042d39ca22e30963415fea77fcc2b1ff2c2ba300 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Nov 2023 16:34:35 +0000 Subject: [PATCH 35/63] [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.1.3 → v0.1.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.3...v0.1.4) --- .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 b7ef9531..346b476e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -59,7 +59,7 @@ repos: hooks: - id: isort - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.1.3' + rev: 'v0.1.4' hooks: - id: ruff - repo: https://github.com/PyCQA/flake8 From df46955943e119fbf5b9b6af75da18e0066a749d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Nov 2023 16:37:59 +0000 Subject: [PATCH 36/63] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/conf.py | 2 +- examples/CreateKml.py | 2 +- fastkml/base.py | 13 +++-- fastkml/data.py | 22 +++++---- fastkml/geometry.py | 37 ++++++++------ fastkml/gx.py | 32 ++++++------ fastkml/kml.py | 67 ++++++++++++++------------ fastkml/styles.py | 16 +++--- fastkml/times.py | 9 ++-- fastkml/views.py | 4 +- tests/atom_test.py | 12 ++--- tests/base.py | 2 +- tests/base_test.py | 8 +-- tests/config_test.py | 4 +- tests/data_test.py | 24 ++++----- tests/geometries/linearring_test.py | 2 +- tests/geometries/linestring_test.py | 4 +- tests/geometries/multigeometry_test.py | 10 ++-- tests/geometries/point_test.py | 4 +- tests/gx_test.py | 20 ++++---- tests/kml_test.py | 2 +- tests/oldunit_test.py | 50 ++++++++++--------- tests/styles_test.py | 16 +++--- tests/times_test.py | 8 +-- tests/views_test.py | 12 ++--- 25 files changed, 200 insertions(+), 182 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 1cd2dcf1..342f0a47 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -236,7 +236,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ("index", "fastkml", "FastKML Documentation", ["Christian Ledermann & Ian Lee"], 1) + ("index", "fastkml", "FastKML Documentation", ["Christian Ledermann & Ian Lee"], 1), ] # If true, show URL addresses after external links. diff --git a/examples/CreateKml.py b/examples/CreateKml.py index 81595dfe..6906825b 100644 --- a/examples/CreateKml.py +++ b/examples/CreateKml.py @@ -5,7 +5,7 @@ # Create the root KML object k = kml.KML() -ns = "{http://www.opengis.net/kml/2.2}" # noqa: FS003 +ns = "{http://www.opengis.net/kml/2.2}" # Create a KML Document and add it to the KML root object d = kml.Document(ns, "docid", "doc name", "doc description") diff --git a/fastkml/base.py b/fastkml/base.py index 048fbf91..de2648d1 100644 --- a/fastkml/base.py +++ b/fastkml/base.py @@ -68,11 +68,11 @@ def etree_element( """Return the KML Object as an Element.""" if self.__name__: element: Element = config.etree.Element( # type: ignore[attr-defined] - f"{self.ns}{self.__name__}" + f"{self.ns}{self.__name__}", ) else: raise NotImplementedError( - "Call of abstract base class, subclasses implement this!" + "Call of abstract base class, subclasses implement this!", ) for mapping in self.kml_object_mapping: mapping["to_kml"](self, element, **mapping) @@ -98,7 +98,7 @@ def from_string(self, xml_string: str) -> None: making it a classmethod. """ self.from_element( - cast(Element, config.etree.XML(xml_string)) # type: ignore[attr-defined] + cast(Element, config.etree.XML(xml_string)), # type: ignore[attr-defined] ) def to_string( @@ -125,7 +125,7 @@ def to_string( return cast( str, config.etree.tostring( # type: ignore[attr-defined] - self.etree_element(), encoding="UTF-8" + self.etree_element(), encoding="UTF-8", ).decode("UTF-8"), ) @@ -168,12 +168,15 @@ def class_from_string( ns: Optional[str] = None, strict: bool = True, ) -> "_XMLObject": - """Creates a geometry object from a string. + """ + Creates a geometry object from a string. Args: + ---- string: String representation of the geometry object Returns: + ------- Geometry object """ ns = cls._get_ns(ns) diff --git a/fastkml/data.py b/fastkml/data.py index ef94671d..ac553e2e 100644 --- a/fastkml/data.py +++ b/fastkml/data.py @@ -13,7 +13,8 @@ # 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 -"""Add Custom Data +""" +Add Custom Data https://developers.google.com/kml/documentation/extendeddata#example """ @@ -27,7 +28,7 @@ from typing import Tuple from typing import Union -import fastkml.config as config +from fastkml import config from fastkml.base import _BaseObject from fastkml.base import _XMLObject from fastkml.enums import DataType @@ -135,13 +136,13 @@ def etree_element( element.set("name", self.name) for simple_field in self.simple_fields: sf = config.etree.SubElement( # type: ignore[attr-defined] - element, f"{self.ns}SimpleField" + element, f"{self.ns}SimpleField", ) sf.set("type", simple_field.type.value) sf.set("name", simple_field.name) if simple_field.display_name: dn = config.etree.SubElement( # type: ignore[attr-defined] - sf, f"{self.ns}displayName" + sf, f"{self.ns}displayName", ) dn.text = simple_field.display_name return element @@ -179,7 +180,7 @@ def _get_kwargs( kwargs = super()._get_kwargs(ns=ns, element=element, strict=strict) kwargs["name"] = element.get("name") kwargs["fields"] = cls._get_fields_kwargs_from_element( - ns=ns, element=element, strict=strict + ns=ns, element=element, strict=strict, ) return kwargs @@ -218,12 +219,12 @@ def etree_element( element = super().etree_element(precision=precision, verbosity=verbosity) element.set("name", self.name or "") value = config.etree.SubElement( # type: ignore[attr-defined] - element, f"{self.ns}value" + element, f"{self.ns}value", ) value.text = self.value if self.display_name: display_name = config.etree.SubElement( # type: ignore[attr-defined] - element, f"{self.ns}displayName" + element, f"{self.ns}displayName", ) display_name.text = self.display_name return element @@ -308,7 +309,7 @@ def etree_element( element.set("schemaUrl", self.schema_url) for data in self.data: sd = config.etree.SubElement( # type: ignore[attr-defined] - element, f"{self.ns}SimpleData" + element, f"{self.ns}SimpleData", ) sd.set("name", data.name) sd.text = data.value @@ -333,7 +334,8 @@ def _get_kwargs( class ExtendedData(_XMLObject): - """Represents a list of untyped name/value pairs. See docs: + """ + Represents a list of untyped name/value pairs. See docs: -> 'Adding Untyped Name/Value Pairs' https://developers.google.com/kml/documentation/extendeddata @@ -384,7 +386,7 @@ def _get_kwargs( typed_data = element.findall(f"{ns}SchemaData") for sd in typed_data: el_schema_data = SchemaData.class_from_element( - ns=ns, element=sd, strict=strict + ns=ns, element=sd, strict=strict, ) elements.append(el_schema_data) kwargs["elements"] = elements diff --git a/fastkml/geometry.py b/fastkml/geometry.py index aa7b1f5e..6bcffe25 100644 --- a/fastkml/geometry.py +++ b/fastkml/geometry.py @@ -53,13 +53,14 @@ GeometryType = Union[geo.Polygon, geo.LineString, geo.LinearRing, geo.Point] MultiGeometryType = Union[ - geo.MultiPoint, geo.MultiLineString, geo.MultiPolygon, geo.GeometryCollection + geo.MultiPoint, geo.MultiLineString, geo.MultiPolygon, geo.GeometryCollection, ] AnyGeometryType = Union[GeometryType, MultiGeometryType] class _Geometry(_BaseObject): - """Baseclass with common methods for all geometry objects. + """ + Baseclass with common methods for all geometry objects. Attributes: extrude: boolean --> Specifies whether to connect the feature to the ground with a line. @@ -84,6 +85,7 @@ def __init__( """ Args: + ---- ns: Namespace of the object id: Id of the object target_id: Target id of the object @@ -159,7 +161,7 @@ def _etree_coordinates( def _set_altitude_mode(self, element: Element) -> None: if self.altitude_mode: am_element = config.etree.SubElement( # type: ignore[attr-defined] - element, f"{self.ns}altitudeMode" + element, f"{self.ns}altitudeMode", ) am_element.text = self.altitude_mode.value @@ -168,7 +170,7 @@ def _set_extrude(self, element: Element) -> None: et_element = cast( Element, config.etree.SubElement( # type: ignore[attr-defined] - element, f"{self.ns}extrude" + element, f"{self.ns}extrude", ), ) et_element.text = str(int(self.extrude)) @@ -178,7 +180,7 @@ def _set_tessellate(self, element: Element) -> None: t_element = cast( Element, config.etree.SubElement( # type: ignore[attr-defined] - element, f"{self.ns}tessellate" + element, f"{self.ns}tessellate", ), ) t_element.text = str(int(self.tessellate)) @@ -197,7 +199,7 @@ def etree_element( @classmethod def _get_coordinates( - cls, *, ns: str, element: Element, strict: bool + cls, *, ns: str, element: Element, strict: bool, ) -> List[PointType]: """ Get coordinates from element. @@ -279,7 +281,7 @@ def _get_geometry_kwargs( "extrude": cls._get_extrude(ns=ns, element=element, strict=strict), "tessellate": cls._get_tessellate(ns=ns, element=element, strict=strict), "altitude_mode": cls._get_altitude_mode( - ns=ns, element=element, strict=strict + ns=ns, element=element, strict=strict, ), } @@ -304,7 +306,7 @@ def _get_kwargs( kwargs = super()._get_kwargs(ns=ns, element=element, strict=strict) kwargs.update(cls._get_geometry_kwargs(ns=ns, element=element, strict=strict)) kwargs.update( - {"geometry": cls._get_geometry(ns=ns, element=element, strict=strict)} + {"geometry": cls._get_geometry(ns=ns, element=element, strict=strict)}, ) return kwargs @@ -496,8 +498,8 @@ def etree_element( ) outer_boundary.append( linear_ring(geometry=self.geometry.exterior).etree_element( - precision=precision, verbosity=verbosity - ) + precision=precision, verbosity=verbosity, + ), ) for interior in self.geometry.interiors: inner_boundary = cast( @@ -509,8 +511,8 @@ def etree_element( ) inner_boundary.append( linear_ring(geometry=interior).etree_element( - precision=precision, verbosity=verbosity - ) + precision=precision, verbosity=verbosity, + ), ) return element @@ -541,7 +543,7 @@ def _get_geometry(cls, *, ns: str, element: Element, strict: bool) -> geo.Polygo ).decode("UTF-8") raise KMLParseError(f"Missing LinearRing in {error}") interiors.append( - LinearRing._get_geometry(ns=ns, element=inner_ring, strict=strict) + LinearRing._get_geometry(ns=ns, element=inner_ring, strict=strict), ) return geo.Polygon.from_linear_rings(exterior, *interiors) @@ -549,12 +551,15 @@ def _get_geometry(cls, *, ns: str, element: Element, strict: bool) -> geo.Polygo def create_multigeometry( geometries: Sequence[AnyGeometryType], ) -> Optional[MultiGeometryType]: - """Create a MultiGeometry from a sequence of geometries. + """ + Create a MultiGeometry from a sequence of geometries. Args: + ---- geometries: Sequence of geometries. Returns: + ------- MultiGeometry """ @@ -633,13 +638,13 @@ def etree_element( tessellate=None, altitude_mode=None, geometry=geometry, # type: ignore[arg-type] - ).etree_element(precision=precision, verbosity=verbosity) + ).etree_element(precision=precision, verbosity=verbosity), ) return element @classmethod def _get_geometry( - cls, *, ns: str, element: Element, strict: bool + cls, *, ns: str, element: Element, strict: bool, ) -> Optional[MultiGeometryType]: geometries = [] allowed_geometries = (cls,) + tuple(cls.map_to_kml.values()) diff --git a/fastkml/gx.py b/fastkml/gx.py index f9f5fb27..432f9da1 100644 --- a/fastkml/gx.py +++ b/fastkml/gx.py @@ -91,7 +91,7 @@ import arrow import pygeoif.geometry as geo -import fastkml.config as config +from fastkml import config from fastkml.enums import AltitudeMode from fastkml.enums import Verbosity from fastkml.geometry import _Geometry @@ -144,30 +144,30 @@ def etree_elements( name_spaces = name_spaces or {} name_spaces = {**config.NAME_SPACES, **name_spaces} element: Element = config.etree.Element( # type: ignore[attr-defined] - f"{name_spaces.get('kml', '')}when" + f"{name_spaces.get('kml', '')}when", ) if self.when: element.text = self.when.isoformat() yield element element = config.etree.Element( # type: ignore[attr-defined] - f"{name_spaces.get('gx', '')}coord" + f"{name_spaces.get('gx', '')}coord", ) if self.coord: element.text = " ".join([str(c) for c in self.coord.coords[0]]) yield element element = config.etree.Element( # type: ignore[attr-defined] - f"{name_spaces.get('gx', '')}angles" + f"{name_spaces.get('gx', '')}angles", ) if self.angle: element.text = " ".join( - [str(self.angle.heading), str(self.angle.tilt), str(self.angle.roll)] + [str(self.angle.heading), str(self.angle.tilt), str(self.angle.roll)], ) yield element def track_items_to_geometry(track_items: Sequence[TrackItem]) -> geo.LineString: return geo.LineString.from_points( - *[item.coord for item in track_items if item.coord is not None] + *[item.coord for item in track_items if item.coord is not None], ) @@ -242,7 +242,7 @@ def etree_element( if self.track_items: for track_item in self.track_items: for track_item_element in track_item.etree_elements( - precision=precision, verbosity=verbosity, name_spaces=name_spaces + precision=precision, verbosity=verbosity, name_spaces=name_spaces, ): element.append(track_item_element) return element @@ -265,7 +265,7 @@ def track_items_kwargs_from_element( for coord in element.findall(f"{config.GXNS}coord"): if coord is not None and coord.text: coords.append( - geo.Point(*[float(c) for c in coord.text.strip().split()]) + geo.Point(*[float(c) for c in coord.text.strip().split()]), ) else: coords.append(None) @@ -290,20 +290,20 @@ def _get_kwargs( ) -> Dict[str, Any]: kwargs = super()._get_kwargs(ns=ns, element=element, strict=strict) kwargs["track_items"] = cls.track_items_kwargs_from_element( - ns=ns, element=element, strict=strict + ns=ns, element=element, strict=strict, ) return kwargs def multilinestring_to_tracks( - multilinestring: geo.MultiLineString, ns: Optional[str] + multilinestring: geo.MultiLineString, ns: Optional[str], ) -> List[Track]: return [Track(ns=ns, geometry=linestring) for linestring in multilinestring.geoms] def tracks_to_geometry(tracks: Sequence[Track]) -> geo.MultiLineString: return geo.MultiLineString.from_linestrings( - *[cast(geo.LineString, track.geometry) for track in tracks if track.geometry] + *[cast(geo.LineString, track.geometry) for track in tracks if track.geometry], ) @@ -365,15 +365,15 @@ def etree_element( i_element = cast( Element, config.etree.SubElement( # type: ignore[attr-defined] - element, f"{self.ns}interpolate" + element, f"{self.ns}interpolate", ), ) i_element.text = str(int(self.interpolate)) for track in self.tracks or []: element.append( track.etree_element( - precision=precision, verbosity=verbosity, name_spaces=name_spaces - ) + precision=precision, verbosity=verbosity, name_spaces=name_spaces, + ), ) return element @@ -423,9 +423,9 @@ def _get_kwargs( ) -> Dict[str, Any]: kwargs = super()._get_kwargs(ns=ns, element=element, strict=strict) kwargs["interpolate"] = cls._get_interpolate( - ns=ns, element=element, strict=strict + ns=ns, element=element, strict=strict, ) kwargs["tracks"] = cls._get_track_kwargs_from_element( - ns=config.GXNS, element=element, strict=strict + ns=config.GXNS, element=element, strict=strict, ) return kwargs diff --git a/fastkml/kml.py b/fastkml/kml.py index 603ac48e..893ffa5b 100644 --- a/fastkml/kml.py +++ b/fastkml/kml.py @@ -31,9 +31,9 @@ from typing import Optional from typing import Union -import fastkml.atom as atom -import fastkml.config as config -import fastkml.gx as gx +from fastkml import atom +from fastkml import config +from fastkml import gx from fastkml.base import _BaseObject from fastkml.data import ExtendedData from fastkml.data import Schema @@ -199,14 +199,16 @@ def __init__( @property def style_url(self) -> Optional[str]: - """Returns the url only, not a full StyleUrl object. - if you need the full StyleUrl object use _style_url""" + """ + Returns the url only, not a full StyleUrl object. + if you need the full StyleUrl object use _style_url + """ if isinstance(self._style_url, StyleUrl): return self._style_url.url @style_url.setter def style_url(self, styleurl: Union[str, StyleUrl, None]) -> None: - """you may pass a StyleUrl Object, a string or None""" + """You may pass a StyleUrl Object, a string or None""" if isinstance(styleurl, StyleUrl): self._style_url = styleurl elif isinstance(styleurl, str): @@ -270,14 +272,14 @@ def author(self, name): raise TypeError def append_style(self, style: Union[Style, StyleMap]) -> None: - """append a style to the feature""" + """Append a style to the feature""" if isinstance(style, _StyleSelector): self._styles.append(style) else: raise TypeError def styles(self) -> Iterator[Union[Style, StyleMap]]: - """iterate over the styles of this feature""" + """Iterate over the styles of this feature""" for style in self._styles: if isinstance(style, _StyleSelector): yield style @@ -287,7 +289,7 @@ def styles(self) -> Iterator[Union[Style, StyleMap]]: @property def snippet(self): if not self._snippet: - return + return None if isinstance(self._snippet, dict): text = self._snippet.get("text") if text: @@ -303,7 +305,7 @@ def snippet(self): else: raise ValueError( "Snippet must be dict of " - "{'text':t, 'maxLines':i} or string" # noqa: FS003 + "{'text':t, 'maxLines':i} or string", ) @snippet.setter @@ -321,7 +323,7 @@ def snippet(self, snip=None): else: raise ValueError( "Snippet must be dict of " - "{'text':t, 'maxLines':i} or string" # noqa: FS003 + "{'text':t, 'maxLines':i} or string", ) @property @@ -722,22 +724,22 @@ def etree_element( refresh_mode.text = self._refresh_mode if self._refresh_interval: refresh_interval = config.etree.SubElement( - element, f"{self.ns}refreshInterval" + element, f"{self.ns}refreshInterval", ) refresh_interval.text = str(self._refresh_interval) if self._view_refresh_mode: view_refresh_mode = config.etree.SubElement( - element, f"{self.ns}viewRefreshMode" + element, f"{self.ns}viewRefreshMode", ) view_refresh_mode.text = self._view_refresh_mode if self._view_refresh_time: view_refresh_time = config.etree.SubElement( - element, f"{self.ns}viewRefreshTime" + element, f"{self.ns}viewRefreshTime", ) view_refresh_time.text = str(self._view_refresh_time) if self._view_bound_scale: view_bound_scale = config.etree.SubElement( - element, f"{self.ns}viewBoundScale" + element, f"{self.ns}viewBoundScale", ) view_bound_scale.text = str(self._view_bound_scale) if self._view_format: @@ -829,14 +831,14 @@ def __init__( self._features = features or [] def features(self) -> Iterator[_Feature]: - """iterate over features""" + """Iterate over features""" for feature in self._features: if isinstance(feature, (Folder, Placemark, Document, _Overlay)): yield feature else: raise TypeError( "Features must be instances of " - "(Folder, Placemark, Document, Overlay)" + "(Folder, Placemark, Document, Overlay)", ) def etree_element( @@ -850,7 +852,7 @@ def etree_element( return element def append(self, kmlobj: _Feature) -> None: - """append a feature""" + """Append a feature""" if id(kmlobj) == id(self): raise ValueError("Cannot append self") if isinstance(kmlobj, (Folder, Placemark, Document, _Overlay)): @@ -858,7 +860,7 @@ def append(self, kmlobj: _Feature) -> None: else: raise TypeError( "Features must be instances of " - "(Folder, Placemark, Document, Overlay)" + "(Folder, Placemark, Document, Overlay)", ) @@ -1224,7 +1226,7 @@ def shape(self, value): elif value is None: self._shape = None else: - raise ValueError("Shape must be one of " "rectangle, cylinder, sphere") + raise ValueError("Shape must be one of rectangle, cylinder, sphere") def view_volume(self, left_fov, right_fov, bottom_fov, top_fov, near): self.left_fov = left_fov @@ -1255,7 +1257,7 @@ def etree_element( self._bottom_fov, self._top_fov, self._near, - ] + ], ): view_volume = config.etree.SubElement(element, f"{self.ns}ViewVolume") left_fov = config.etree.SubElement(view_volume, f"{self.ns}leftFov") @@ -1476,7 +1478,7 @@ def rotation(self, value): raise ValueError def lat_lon_box( - self, north: int, south: int, east: int, west: int, rotation: int = 0 + self, north: int, south: int, east: int, west: int, rotation: int = 0, ) -> None: if -90 <= float(north) <= 90: self.north = north @@ -1510,7 +1512,7 @@ def etree_element( altitude.text = self._altitude if self._altitude_mode: altitude_mode = config.etree.SubElement( - element, f"{self.ns}altitudeMode" + element, f"{self.ns}altitudeMode", ) altitude_mode.text = self._altitude_mode if all([self._north, self._south, self._east, self._west]): @@ -1768,7 +1770,8 @@ class KML: ns = None def __init__(self, ns: Optional[str] = None) -> None: - """The namespace (ns) may be empty ('') if the 'kml:' prefix is + """ + The namespace (ns) may be empty ('') if the 'kml:' prefix is undesired. Note that all child elements like Document or Placemark need to be initialized with empty namespace as well in this case. @@ -1778,10 +1781,10 @@ def __init__(self, ns: Optional[str] = None) -> None: self.ns = config.KMLNS if ns is None else ns def from_string(self, xml_string: str) -> None: - """create a KML object from a xml string""" + """Create a KML object from a xml string""" try: element = config.etree.fromstring( - xml_string, parser=config.etree.XMLParser(huge_tree=True, recover=True) + xml_string, parser=config.etree.XMLParser(huge_tree=True, recover=True), ) except TypeError: element = config.etree.XML(xml_string) @@ -1830,7 +1833,7 @@ def etree_element( else: try: root = config.etree.Element( - f"{self.ns}kml", nsmap={None: self.ns[1:-1]} + f"{self.ns}kml", nsmap={None: self.ns[1:-1]}, ) except TypeError: root = config.etree.Element(f"{self.ns}kml") @@ -1848,29 +1851,29 @@ def to_string(self, prettyprint: bool = False) -> str: ).decode("UTF-8") except TypeError: return config.etree.tostring(self.etree_element(), encoding="UTF-8").decode( - "UTF-8" + "UTF-8", ) def features(self) -> Iterator[Union[Folder, Document, Placemark]]: - """iterate over features""" + """Iterate over features""" for feature in self._features: if isinstance(feature, (Document, Folder, Placemark, _Overlay)): yield feature else: raise TypeError( "Features must be instances of " - "(Document, Folder, Placemark, Overlay)" + "(Document, Folder, Placemark, Overlay)", ) def append(self, kmlobj: Union[Folder, Document, Placemark]) -> None: - """append a feature""" + """Append a feature""" if id(kmlobj) == id(self): raise ValueError("Cannot append self") if isinstance(kmlobj, (Document, Folder, Placemark, _Overlay)): self._features.append(kmlobj) else: raise TypeError( - "Features must be instances of (Document, Folder, Placemark, Overlay)" + "Features must be instances of (Document, Folder, Placemark, Overlay)", ) diff --git a/fastkml/styles.py b/fastkml/styles.py index 48240c65..8698c019 100644 --- a/fastkml/styles.py +++ b/fastkml/styles.py @@ -181,7 +181,7 @@ def __init__( hot_spot: Optional[HotSpot] = None, ) -> None: super().__init__( - ns=ns, id=id, target_id=target_id, color=color, color_mode=color_mode + ns=ns, id=id, target_id=target_id, color=color, color_mode=color_mode, ) self.scale = scale @@ -219,7 +219,7 @@ def etree_element( href.text = self.icon_href if self.hot_spot: hot_spot = config.etree.SubElement( # type: ignore[attr-defined] - element, f"{self.ns}hotSpot" + element, f"{self.ns}hotSpot", ) hot_spot.attrib["x"] = str(self.hot_spot["x"]) hot_spot.attrib["y"] = str(self.hot_spot["y"]) @@ -272,7 +272,7 @@ def __init__( width: Union[int, float] = 1, ) -> None: super().__init__( - ns=ns, id=id, target_id=target_id, color=color, color_mode=color_mode + ns=ns, id=id, target_id=target_id, color=color, color_mode=color_mode, ) self.width = width @@ -322,7 +322,7 @@ def __init__( outline: int = 1, ) -> None: super().__init__( - ns=ns, id=id, target_id=target_id, color=color, color_mode=color_mode + ns=ns, id=id, target_id=target_id, color=color, color_mode=color_mode, ) self.fill = fill self.outline = outline @@ -384,7 +384,7 @@ def __init__( scale: float = 1.0, ) -> None: super().__init__( - ns=ns, id=id, target_id=target_id, color=color, color_mode=color_mode + ns=ns, id=id, target_id=target_id, color=color, color_mode=color_mode, ) self.scale = scale @@ -410,9 +410,11 @@ def from_element(self, element: Element) -> None: class BalloonStyle(_BaseObject): - """Specifies how the description balloon for placemarks is drawn. + """ + Specifies how the description balloon for placemarks is drawn. The , if specified, is used as the background color of - the balloon.""" + the balloon. + """ __name__ = "BalloonStyle" diff --git a/fastkml/times.py b/fastkml/times.py index 980e157f..5f61cb92 100644 --- a/fastkml/times.py +++ b/fastkml/times.py @@ -22,7 +22,7 @@ import arrow -import fastkml.config as config +from fastkml import config from fastkml.base import _BaseObject from fastkml.enums import DateTimeResolution from fastkml.enums import Verbosity @@ -32,12 +32,13 @@ # year and month may be separated by a dash or not # year is always 4 digits, month is always 2 digits year_month_day = re.compile( - r"^(?P\d{4})(?:-)?(?P\d{2})?(?:-)?(?P\d{2})?$" + r"^(?P\d{4})(?:-)?(?P\d{2})?(?:-)?(?P\d{2})?$", ) class KmlDateTime: - """A KML DateTime object. + """ + A KML DateTime object. This class is used to parse and format KML DateTime objects. @@ -170,7 +171,7 @@ def etree_element( ) -> Element: element = super().etree_element(precision=precision, verbosity=verbosity) when = config.etree.SubElement( # type: ignore[attr-defined] - element, f"{self.ns}when" + element, f"{self.ns}when", ) when.text = str(self.timestamp) return element diff --git a/fastkml/views.py b/fastkml/views.py index 1fffea36..8e9973f8 100644 --- a/fastkml/views.py +++ b/fastkml/views.py @@ -3,8 +3,8 @@ from typing import SupportsFloat from typing import Union -import fastkml.config as config -import fastkml.gx as gx +from fastkml import config +from fastkml import gx from fastkml.base import _BaseObject from fastkml.enums import Verbosity from fastkml.mixins import TimeMixin diff --git a/tests/atom_test.py b/tests/atom_test.py index 09e98fdd..d059e1c6 100644 --- a/tests/atom_test.py +++ b/tests/atom_test.py @@ -25,11 +25,11 @@ class TestStdLibrary(StdLibrary): """Test with the standard library.""" def test_atom_link_ns(self) -> None: - ns = "{http://www.opengis.net/kml/2.2}" # noqa: FS003 + ns = "{http://www.opengis.net/kml/2.2}" link = atom.Link(ns=ns) assert link.ns == ns assert link.to_string().startswith( - ' None: @@ -57,7 +57,7 @@ def test_atom_link_read(self) -> None: link.from_string( '' + 'title="Title" length="3456" />', ) assert link.href == "#here" assert link.rel == "alternate" @@ -71,18 +71,18 @@ def test_atom_link_read_no_href(self) -> None: link.from_string( '' + 'title="Title" length="3456" />', ) assert link.href is None def test_atom_person_ns(self) -> None: - ns = "{http://www.opengis.net/kml/2.2}" # noqa: FS003 + ns = "{http://www.opengis.net/kml/2.2}" p = atom._Person(ns=ns) assert p.ns == ns def test_atom_author(self) -> None: a = atom.Author( - name="Nobody", uri="http://localhost", email="cl@donotreply.com" + name="Nobody", uri="http://localhost", email="cl@donotreply.com", ) serialized = a.to_string() diff --git a/tests/base.py b/tests/base.py index ec99d98a..5327191f 100644 --- a/tests/base.py +++ b/tests/base.py @@ -20,7 +20,7 @@ import pytest try: # pragma: no cover - import lxml # noqa: F401 + import lxml LXML = True except ImportError: # pragma: no cover diff --git a/tests/base_test.py b/tests/base_test.py index d6362198..78eb7bba 100644 --- a/tests/base_test.py +++ b/tests/base_test.py @@ -43,7 +43,7 @@ def test_to_str_empty_ns(self) -> None: obj.__name__ = "test" assert obj.to_string().replace(" ", "").replace( - "\n", "" + "\n", "", ) == ''.replace(" ", "") def test_from_string(self) -> None: @@ -54,7 +54,7 @@ def test_from_string(self) -> None: xml_string=( '' - ) + ), ) assert be.id == "id-0" @@ -88,7 +88,7 @@ def test_base_from_string_raises(self) -> None: with pytest.raises(TypeError): be.from_string( - '' + '', ) def test_base_class_from_string(self) -> None: @@ -126,7 +126,7 @@ def test_from_string(self) -> None: xml_string=( '\n' - ) + ), ) assert be.id == "id-0" diff --git a/tests/config_test.py b/tests/config_test.py index 0fdbd3ee..c799e5a0 100644 --- a/tests/config_test.py +++ b/tests/config_test.py @@ -58,11 +58,11 @@ def test_register_namespaces() -> None: def test_default_registered_namespaces() -> None: - assert config.DEFAULT_NAME_SPACES == { + assert { "kml": "http://www.opengis.net/kml/2.2", "atom": "http://www.w3.org/2005/Atom", "gx": "http://www.google.com/kml/ext/2.2", - } + } == config.DEFAULT_NAME_SPACES def test_set_default_namespaces() -> None: diff --git a/tests/data_test.py b/tests/data_test.py index 3debd182..4b28b129 100644 --- a/tests/data_test.py +++ b/tests/data_test.py @@ -32,11 +32,11 @@ def test_schema_requires_id(self) -> None: pytest.raises(KMLSchemaError, kml.Schema, "") def test_schema(self) -> None: - ns = "{http://www.opengis.net/kml/2.2}" # noqa: FS003 + ns = "{http://www.opengis.net/kml/2.2}" s = kml.Schema(ns, "some_id") assert not list(s.simple_fields) field = data.SimpleField( - name="Integer", type=DataType.int_, display_name="An Integer" + name="Integer", type=DataType.int_, display_name="An Integer", ) s.append(field) assert s.simple_fields[0] == field @@ -94,7 +94,7 @@ def test_schema_from_string(self) -> None: assert k1.to_string() == k.to_string() def test_schema_data(self) -> None: - ns = "{http://www.opengis.net/kml/2.2}" # noqa: FS003 + ns = "{http://www.opengis.net/kml/2.2}" pytest.raises(ValueError, data.SchemaData, ns) pytest.raises(ValueError, data.SchemaData, ns, "") sd = data.SchemaData(ns, "#default") @@ -102,11 +102,11 @@ def test_schema_data(self) -> None: assert len(sd.data) == 1 sd.append_data(data.SimpleData(value=1, name="Integer")) assert len(sd.data) == 2 - assert sd.data[0] == data.SimpleData(**{"value": "Some Text", "name": "text"}) - assert sd.data[1] == data.SimpleData(**{"value": 1, "name": "Integer"}) + assert sd.data[0] == data.SimpleData(value="Some Text", name="text") + assert sd.data[1] == data.SimpleData(value=1, name="Integer") new_data = ( data.SimpleData("text", "Some new Text"), - data.SimpleData(**{"value": 2, "name": "Integer"}), + data.SimpleData(value=2, name="Integer"), ) sd.data = new_data assert len(sd.data) == 2 @@ -116,7 +116,7 @@ def test_schema_data(self) -> None: assert sd.data[1].value == 2 def test_untyped_extended_data(self) -> None: - ns = "{http://www.opengis.net/kml/2.2}" # noqa: FS003 + ns = "{http://www.opengis.net/kml/2.2}" k = kml.KML(ns=ns) p = kml.Placemark(ns, "id", "name", "description") @@ -125,7 +125,7 @@ def test_untyped_extended_data(self) -> None: elements=[ data.Data(ns=ns, name="info", value="so much to see"), data.Data( - ns=ns, name="weather", display_name="Weather", value="blue skies" + ns=ns, name="weather", display_name="Weather", value="blue skies", ), ], ) @@ -148,17 +148,17 @@ def test_untyped_extended_data(self) -> None: assert extended_data.elements[1].display_name == "Weather" def test_untyped_extended_data_nested(self) -> None: - ns = "{http://www.opengis.net/kml/2.2}" # noqa: FS003 + ns = "{http://www.opengis.net/kml/2.2}" k = kml.KML(ns=ns) d = kml.Document(ns, "docid", "doc name", "doc description") d.extended_data = kml.ExtendedData( - ns=ns, elements=[data.Data(ns=ns, name="type", value="Document")] + ns=ns, elements=[data.Data(ns=ns, name="type", value="Document")], ) f = kml.Folder(ns, "fid", "f name", "f description") f.extended_data = kml.ExtendedData( - ns=ns, elements=[data.Data(ns=ns, name="type", value="Folder")] + ns=ns, elements=[data.Data(ns=ns, name="type", value="Folder")], ) k.append(d) @@ -222,7 +222,7 @@ def test_extended_data(self) -> None: ) sd = extended_data.elements[2] assert sd.data[0] == data.SimpleData( - name="TrailHeadName", value="Mount Everest" + name="TrailHeadName", value="Mount Everest", ) assert sd.data[1] == data.SimpleData(name="TrailLength", value="347.45") assert sd.data[2] == data.SimpleData(name="ElevationGain", value="10000") diff --git a/tests/geometries/linearring_test.py b/tests/geometries/linearring_test.py index 979a6b75..f935334f 100644 --- a/tests/geometries/linearring_test.py +++ b/tests/geometries/linearring_test.py @@ -55,7 +55,7 @@ def test_from_string(self) -> None: '' "0.000000,0.000000 1.000000,0.000000 1.0,1.0 " "0.000000,0.000000" - "" + "", ), ) diff --git a/tests/geometries/linestring_test.py b/tests/geometries/linestring_test.py index e1351686..736d39e3 100644 --- a/tests/geometries/linestring_test.py +++ b/tests/geometries/linestring_test.py @@ -58,12 +58,12 @@ def test_from_string(self) -> None: "" "-122.364383,37.824664,0 -122.364152,37.824322,0" "" - "" + "", ), ) assert linestring.geometry == geo.LineString( - ((-122.364383, 37.824664, 0), (-122.364152, 37.824322, 0)) + ((-122.364383, 37.824664, 0), (-122.364152, 37.824322, 0)), ) diff --git a/tests/geometries/multigeometry_test.py b/tests/geometries/multigeometry_test.py index 00c26437..10d279e4 100644 --- a/tests/geometries/multigeometry_test.py +++ b/tests/geometries/multigeometry_test.py @@ -312,9 +312,9 @@ def test_multi_geometries_read(self) -> None: ), ), geo.LinearRing( - ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)) + ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)), ), - ) + ), ), geo.MultiPolygon( ( @@ -337,12 +337,12 @@ def test_multi_geometries_read(self) -> None: ), ), (((0.0, 0.0), (0.0, 2.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)),), - ) + ), ), geo.MultiLineString( - (((1.0, 2.0), (3.0, 4.0)), ((5.0, 6.0), (7.0, 8.0))) + (((1.0, 2.0), (3.0, 4.0)), ((5.0, 6.0), (7.0, 8.0))), ), - ) + ), ) def test_empty_multi_geometries_read(self) -> None: diff --git a/tests/geometries/point_test.py b/tests/geometries/point_test.py index 2e60c87e..dde8ce3a 100644 --- a/tests/geometries/point_test.py +++ b/tests/geometries/point_test.py @@ -54,7 +54,7 @@ def test_to_string_empty_geometry(self) -> None: point = Point(geometry=geo.Point(None, None)) # type: ignore[arg-type] with pytest.raises( - KMLWriteError, match=r"Invalid dimensions in coordinates '\(\(\),\)'" + KMLWriteError, match=r"Invalid dimensions in coordinates '\(\(\),\)'", ): point.to_string() @@ -65,7 +65,7 @@ def test_from_string(self) -> None: Point.class_from_string( '' "1.000000,2.000000" - "" + "", ), ) diff --git a/tests/gx_test.py b/tests/gx_test.py index 2269bb60..1212b0bf 100644 --- a/tests/gx_test.py +++ b/tests/gx_test.py @@ -72,7 +72,7 @@ def test_multitrack(self) -> None: mt = MultiTrack.class_from_string(doc, ns="") assert mt.geometry == geo.MultiLineString( - (((0.0, 0.0), (1.0, 0.0)), ((0.0, 1.0), (1.0, 1.0))) + (((0.0, 0.0), (1.0, 0.0)), ((0.0, 1.0), (1.0, 1.0))), ) assert "when>" in mt.to_string() assert ( @@ -95,14 +95,14 @@ def test_multitrack(self) -> None: track_items=[ TrackItem( when=datetime.datetime( - 2020, 1, 1, 0, 0, tzinfo=tzutc() + 2020, 1, 1, 0, 0, tzinfo=tzutc(), ), coord=geo.Point(0.0, 0.0), angle=None, ), TrackItem( when=datetime.datetime( - 2020, 1, 1, 0, 10, tzinfo=tzutc() + 2020, 1, 1, 0, 10, tzinfo=tzutc(), ), coord=geo.Point(1.0, 0.0), angle=None, @@ -119,14 +119,14 @@ def test_multitrack(self) -> None: track_items=[ TrackItem( when=datetime.datetime( - 2020, 1, 1, 0, 10, tzinfo=tzutc() + 2020, 1, 1, 0, 10, tzinfo=tzutc(), ), coord=geo.Point(0.0, 1.0), angle=None, ), TrackItem( when=datetime.datetime( - 2020, 1, 1, 0, 20, tzinfo=tzutc() + 2020, 1, 1, 0, 20, tzinfo=tzutc(), ), coord=geo.Point(1.0, 1.0), angle=None, @@ -313,7 +313,7 @@ def test_track_from_str(self) -> None: (-122.203451, 37.374706, 141.800003), (-122.203329, 37.37478, 141.199997), (-122.203207, 37.374857, 140.199997), - ) + ), ) assert track.to_string() == expected_track.to_string() @@ -362,7 +362,7 @@ def test_from_multilinestring(self) -> None: ], ), ], - ) + ), ) def test_multitrack(self) -> None: @@ -384,14 +384,14 @@ def test_multitrack(self) -> None: track_items=[ TrackItem( when=datetime.datetime( - 2010, 5, 28, 2, 2, 55, tzinfo=tzutc() + 2010, 5, 28, 2, 2, 55, tzinfo=tzutc(), ), coord=geo.Point(-122.203451, 37.374706, 141.800003), angle=Angle(heading=1.0, tilt=2.0, roll=3.0), ), TrackItem( when=datetime.datetime( - 2010, 5, 28, 2, 2, 56, tzinfo=tzutc() + 2010, 5, 28, 2, 2, 56, tzinfo=tzutc(), ), coord=geo.Point(-122.203329, 37.37478, 141.199997), angle=Angle(heading=1.0, tilt=2.0, roll=3.0), @@ -408,7 +408,7 @@ def test_multitrack(self) -> None: (-122.203451, 37.374706, 141.800003), (-122.203329, 37.37478, 141.199997), ), - ) + ), ) assert "MultiTrack>" in track.to_string() assert "interpolate>1 None: BBOX=[bboxWest],[bboxSouth],[bboxEast],[bboxNorth] clientName=fastkml - """.strip() + """.strip(), ) assert icon.id == "icon-01" diff --git a/tests/oldunit_test.py b/tests/oldunit_test.py index db7f017c..6d277db8 100644 --- a/tests/oldunit_test.py +++ b/tests/oldunit_test.py @@ -35,8 +35,10 @@ class TestBaseClasses: - """BaseClasses must raise a NotImplementedError on etree_element - and a TypeError on from_element""" + """ + BaseClasses must raise a NotImplementedError on etree_element + and a TypeError on from_element + """ def setup_method(self) -> None: """Always test with the same parser.""" @@ -120,7 +122,7 @@ def setup_method(self) -> None: config.set_default_namespaces() def test_kml(self) -> None: - """kml file without contents""" + """Kml file without contents""" k = kml.KML() assert not list(k.features()) assert ( @@ -133,7 +135,7 @@ def test_kml(self) -> None: def test_folder(self) -> None: """KML file with folders""" - ns = "{http://www.opengis.net/kml/2.2}" # noqa: FS003 + ns = "{http://www.opengis.net/kml/2.2}" k = kml.KML() f = kml.Folder(ns, "id", "name", "description") nf = kml.Folder(ns, "nested-id", "nested-name", "nested-description") @@ -149,7 +151,7 @@ def test_folder(self) -> None: assert s == k2.to_string() def test_placemark(self) -> None: - ns = "{http://www.opengis.net/kml/2.2}" # noqa: FS003 + ns = "{http://www.opengis.net/kml/2.2}" k = kml.KML(ns=ns) p = kml.Placemark(ns, "id", "name", "description") # XXX p.geometry = Point(0.0, 0.0, 0.0) @@ -164,7 +166,7 @@ def test_placemark(self) -> None: def test_document(self) -> None: k = kml.KML() - ns = "{http://www.opengis.net/kml/2.2}" # noqa: FS003 + ns = "{http://www.opengis.net/kml/2.2}" d = kml.Document(ns, "docid", "doc name", "doc description") f = kml.Folder(ns, "fid", "f name", "f description") k.append(d) @@ -190,7 +192,7 @@ def test_author(self) -> None: d.author = "Christian Ledermann" assert "Christian Ledermann" in str(d.to_string()) a = atom.Author( - name="Nobody", uri="http://localhost", email="cl@donotreply.com" + name="Nobody", uri="http://localhost", email="cl@donotreply.com", ) d.author = a assert d.author == "Nobody" @@ -593,7 +595,7 @@ def test_address(self) -> None: 1 1600 Amphitheatre Parkway,... - """ + """, ) doc2 = kml.Document() @@ -611,7 +613,7 @@ def test_phone_number(self) -> None: 1 +1 234 567 8901 - """ + """, ) doc2 = kml.Document() @@ -644,7 +646,7 @@ def test_groundoverlay(self) -> None: - """ + """, ) doc2 = kml.KML() @@ -659,7 +661,7 @@ def test_linarring_placemark(self) -> None: 0.0,0.0 1.0,0.0 1.0,1.0 0.0,0.0 - """ + """, ) doc2 = kml.KML() doc2.from_string(doc.to_string()) @@ -1183,7 +1185,7 @@ def test_get_style_by_url(self) -> None: assert len(list(k.features())) == 1 document = list(k.features())[0] style = document.get_style_by_url( - "http://localhost:8080/somepath#exampleStyleDocument" + "http://localhost:8080/somepath#exampleStyleDocument", ) assert isinstance(list(style.styles())[0], styles.LabelStyle) style = document.get_style_by_url("somepath#linestyleExample") @@ -1514,7 +1516,7 @@ def test_default_to_string(self) -> None: expected.from_string( '' "1" - "" + "", ) assert g.to_string() == expected.to_string() @@ -1534,7 +1536,7 @@ def test_to_string(self) -> None: "" "http://example.com" "" - "" + "", ) assert g.to_string() == expected.to_string() @@ -1549,7 +1551,7 @@ def test_altitude_from_int(self) -> None: "1" "123" "clampToGround" - "" + "", ) assert g.to_string() == expected.to_string() @@ -1564,7 +1566,7 @@ def test_altitude_from_float(self) -> None: "1" "123.4" "clampToGround" - "" + "", ) assert g.to_string() == expected.to_string() @@ -1579,7 +1581,7 @@ def test_altitude_from_string(self) -> None: "1" "123.4" "clampToGround" - "" + "", ) assert g.to_string() == expected.to_string() @@ -1595,7 +1597,7 @@ def test_altitude_mode_absolute(self) -> None: "1" "123.4" "absolute" - "" + "", ) assert g.to_string() == expected.to_string() @@ -1611,7 +1613,7 @@ def test_altitude_mode_unknown_string(self) -> None: "1" "123.4" "clampToGround" - "" + "", ) assert g.to_string() == expected.to_string() @@ -1627,7 +1629,7 @@ def test_altitude_mode_value(self) -> None: "1" "123.4" "clampToGround" - "" + "", ) assert g.to_string() == expected.to_string() @@ -1647,7 +1649,7 @@ def test_latlonbox_no_rotation(self) -> None: "40" "0" "" - "" + "", ) assert g.to_string() == expected.to_string() @@ -1667,7 +1669,7 @@ def test_latlonbox_rotation(self) -> None: "40" "50" "" - "" + "", ) assert g.to_string() == expected.to_string() @@ -1691,7 +1693,7 @@ def test_latlonbox_nswer(self) -> None: "40" "50" "" - "" + "", ) assert g.to_string() == expected.to_string() @@ -1741,7 +1743,7 @@ def test_camera_altitude_mode_absolute(self) -> None: def test_camera_initialization(self) -> None: self.p.camera = kml.Camera( - longitude=10, latitude=20, altitude=30, heading=40, tilt=50, roll=60 + longitude=10, latitude=20, altitude=30, heading=40, tilt=50, roll=60, ) assert self.p.camera.longitude == 10 assert self.p.camera.latitude == 20 diff --git a/tests/styles_test.py b/tests/styles_test.py index 551b4c49..35d8c61e 100644 --- a/tests/styles_test.py +++ b/tests/styles_test.py @@ -39,7 +39,7 @@ def test_style_url_read(self) -> None: url.from_string( '#style-0' + ' id="id-0" targetId="target-0">#style-0', ) assert url.id == "id-0" @@ -80,7 +80,7 @@ def test_icon_style_read(self) -> None: "ff2200ffrandom" "520" "http://example.com/icon.png" - "" + "", ) assert icons.id == "id-1" @@ -119,7 +119,7 @@ def test_line_style_read(self) -> None: " ffaa00ff\n" " normal\n" " 3.0\n" - "\n" + "\n", ) assert lines.id == "id-l0" @@ -159,7 +159,7 @@ def test_poly_style_read(self) -> None: "normal" "1" "0" - "" + "", ) assert ps.id == "id-1" @@ -199,7 +199,7 @@ def test_label_style_read(self) -> None: "ff001122" "normal" "2.2" - "" + "", ) assert ls.id == "id-1" @@ -241,7 +241,7 @@ def test_balloon_style_read(self) -> None: "ff11ff22" "<b>World</b>" "default" - "" + "", ) assert bs.id == "id-7" @@ -350,7 +350,7 @@ def test_style_read(self) -> None: "0" "1" "" - "" + "", ) assert style.id == "id-0" @@ -536,7 +536,7 @@ def test_stylemap_read(self) -> None: - """ + """, ) assert sm.id == "id-sm-0" diff --git a/tests/times_test.py b/tests/times_test.py index c276308b..33081770 100644 --- a/tests/times_test.py +++ b/tests/times_test.py @@ -151,7 +151,7 @@ def test_parse_datetime_with_tz(self) -> None: assert dt.resolution == DateTimeResolution.datetime assert dt.dt == datetime.datetime( - 1997, 7, 16, 7, 30, 15, tzinfo=tzoffset(None, 3600) + 1997, 7, 16, 7, 30, 15, tzinfo=tzoffset(None, 3600), ) def test_parse_datetime_with_tz_no_colon(self) -> None: @@ -159,7 +159,7 @@ def test_parse_datetime_with_tz_no_colon(self) -> None: assert dt.resolution == DateTimeResolution.datetime assert dt.dt == datetime.datetime( - 1997, 7, 16, 7, 30, 15, tzinfo=tzoffset(None, 3600) + 1997, 7, 16, 7, 30, 15, tzinfo=tzoffset(None, 3600), ) def test_parse_datetime_no_tz(self) -> None: @@ -334,7 +334,7 @@ def test_read_timestamp(self) -> None: ts.from_string(doc) assert ts.timestamp.resolution == DateTimeResolution.datetime assert ts.timestamp.dt == datetime.datetime( - 1997, 7, 16, 7, 30, 15, tzinfo=tzutc() + 1997, 7, 16, 7, 30, 15, tzinfo=tzutc(), ) doc = """ @@ -345,7 +345,7 @@ def test_read_timestamp(self) -> None: ts.from_string(doc) assert ts.timestamp.resolution == DateTimeResolution.datetime assert ts.timestamp.dt == datetime.datetime( - 1997, 7, 16, 10, 30, 15, tzinfo=tzoffset(None, 10800) + 1997, 7, 16, 10, 30, 15, tzinfo=tzoffset(None, 10800), ) def test_read_timespan(self) -> None: diff --git a/tests/views_test.py b/tests/views_test.py index 5f1f23b2..dd453fd1 100644 --- a/tests/views_test.py +++ b/tests/views_test.py @@ -60,10 +60,10 @@ def test_create_camera(self) -> None: assert camera.id == "cam-id" assert camera.target_id == "target-cam-id" assert camera.begin == times.KmlDateTime( - datetime.datetime(2019, 1, 1, tzinfo=tzutc()) + datetime.datetime(2019, 1, 1, tzinfo=tzutc()), ) assert camera.end == times.KmlDateTime( - datetime.datetime(2019, 1, 2, tzinfo=tzutc()) + datetime.datetime(2019, 1, 2, tzinfo=tzutc()), ) assert camera.to_string() @@ -99,10 +99,10 @@ def test_camera_read(self) -> None: assert camera.id == "cam-id" assert camera.target_id == "target-cam-id" assert camera.begin == times.KmlDateTime( - datetime.datetime(2019, 1, 1, tzinfo=tzutc()) + datetime.datetime(2019, 1, 1, tzinfo=tzutc()), ) assert camera.end == times.KmlDateTime( - datetime.datetime(2019, 1, 2, tzinfo=tzutc()) + datetime.datetime(2019, 1, 2, tzinfo=tzutc()), ) def test_create_look_at(self) -> None: @@ -132,7 +132,7 @@ def test_create_look_at(self) -> None: assert look_at.id == "look-at-id" assert look_at.target_id == "target-look-at-id" assert look_at._timestamp.timestamp.dt == datetime.datetime( - 2019, 1, 1, tzinfo=tzutc() + 2019, 1, 1, tzinfo=tzutc(), ) assert look_at.begin is None assert look_at.end is None @@ -166,7 +166,7 @@ def test_look_at_read(self) -> None: assert look_at.id == "look-at-id" assert look_at.target_id == "target-look-at-id" assert look_at._timestamp.timestamp.dt == datetime.datetime( - 2019, 1, 1, tzinfo=tzutc() + 2019, 1, 1, tzinfo=tzutc(), ) assert look_at.begin is None assert look_at.end is None From 9927fdce3f9892ec933982c3ad9f9148c3a8eada Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 11 Nov 2023 15:52:44 +0000 Subject: [PATCH 37/63] ruff fixes --- README.rst | 4 +- docs/fastkml.rst | 2 +- fastkml/__init__.py | 15 +- fastkml/about.py | 2 +- fastkml/base.py | 41 +++-- fastkml/data.py | 31 ++-- fastkml/enums.py | 20 ++- fastkml/geometry.py | 73 +++++--- fastkml/gx.py | 32 +++- fastkml/kml.py | 238 ++++++++++++++----------- fastkml/mixins.py | 2 +- fastkml/styles.py | 35 ++-- fastkml/times.py | 8 +- fastkml/views.py | 3 +- pyproject.toml | 8 + tests/data_test.py | 30 ++-- tests/geometries/multigeometry_test.py | 28 +-- tests/geometries/polygon_test.py | 10 +- tests/oldunit_test.py | 137 +++++++------- tests/styles_test.py | 12 +- 20 files changed, 438 insertions(+), 293 deletions(-) diff --git a/README.rst b/README.rst index d398070c..2c5cff80 100644 --- a/README.rst +++ b/README.rst @@ -25,11 +25,11 @@ Fastkml is continually tested :target: http://codecov.io/github/cleder/fastkml?branch=main :alt: codecov.io -.. image:: https://img.shields.io/badge/code%20style-black-000000.svg +.. image:: https://img.shields.io/badge/code_style-black-000000.svg :target: https://github.com/psf/black :alt: Black -.. image:: https://img.shields.io/badge/type%20checker-mypy-blue +.. image:: https://img.shields.io/badge/type_checker-mypy-blue :target: http://mypy-lang.org/ :alt: Mypy diff --git a/docs/fastkml.rst b/docs/fastkml.rst index b218e7db..9d935f0a 100644 --- a/docs/fastkml.rst +++ b/docs/fastkml.rst @@ -7,7 +7,7 @@ Module contents .. automodule:: fastkml :members: :undoc-members: - :noindex: + :no-index: Submodules diff --git a/fastkml/__init__.py b/fastkml/__init__.py index 4b7645cb..21365bbd 100644 --- a/fastkml/__init__.py +++ b/fastkml/__init__.py @@ -15,13 +15,14 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA """ -Fastkml is a library to read, write and manipulate kml files. It aims to keep -it simple and fast (using lxml if available). Fast refers to the time you spend -to write and read KML files as well as the time you spend to get aquainted to -the library or to create KML objects. It provides a subset of KML and is aimed -at documents that can be read from multiple clients such as openlayers and -google maps rather than to give you all functionality that KML on google earth -provides. +Fastkml is a library to read, write and manipulate kml files. + +It aims to keep it simple and fast (using lxml if available). +Fast refers to the time you spend to write and read KML files as well as the time +you spend to get acquainted to the library or to create KML objects. +It provides a subset of KML and is aimed at documents that can be read from +multiple clients such as openlayers and google maps rather than to give you all +functionality that KML on google earth provides. """ from fastkml.about import __version__ # noqa: F401 from fastkml.atom import Author diff --git a/fastkml/about.py b/fastkml/about.py index 53116a36..5496fedc 100644 --- a/fastkml/about.py +++ b/fastkml/about.py @@ -1,5 +1,5 @@ """ -About fastkml +About fastkml. The only purpose of this module is to provide a version number for the package. """ diff --git a/fastkml/base.py b/fastkml/base.py index de2648d1..e2467e20 100644 --- a/fastkml/base.py +++ b/fastkml/base.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012 - 2020 Christian Ledermann +# Copyright (C) 2012 - 2023 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 @@ -14,7 +14,7 @@ # along with this library; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -"""Abstract base classes""" +"""Abstract base classes.""" import logging from typing import Any from typing import Dict @@ -35,24 +35,21 @@ class _XMLObject: """XML Baseclass.""" - _namespaces: Tuple[str, ...] = ("",) + _default_ns: str = "" _node_name: str = "" __name__ = "" + name_spaces: Dict[str, str] kml_object_mapping: Tuple[KmlObjectMap, ...] = () - def __init__(self, ns: Optional[str] = None) -> None: + def __init__( + self, + ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, + ) -> None: """Initialize the XML Object.""" - self.ns: str = self._namespaces[0] if ns is None else ns - - def __eq__(self, other: object) -> bool: - """Compare two XML Objects.""" - if not isinstance(other, self.__class__): - return False - return ( - other.ns == self.ns or other.ns in self._namespaces - if self.ns == "" - else True - ) + self.ns: str = self._default_ns if ns is None else ns + name_spaces = name_spaces or {} + self.name_spaces = {**config.NAME_SPACES, **name_spaces} def __repr__(self) -> str: return f"{self.__class__.__name__}(ns={self.ns})" @@ -71,8 +68,9 @@ def etree_element( f"{self.ns}{self.__name__}", ) else: + msg = "Call of abstract base class, subclasses implement this!" raise NotImplementedError( - "Call of abstract base class, subclasses implement this!", + msg, ) for mapping in self.kml_object_mapping: mapping["to_kml"](self, element, **mapping) @@ -86,7 +84,8 @@ def from_element(self, element: Element) -> None: making it a classmethod. """ if f"{self.ns}{self.__name__}" != element.tag: - raise TypeError("Call of abstract base class, subclasses implement this!") + msg = "Call of abstract base class, subclasses implement this!" + raise TypeError(msg) for mapping in self.kml_object_mapping: mapping["from_kml"](self, element, **mapping) @@ -125,13 +124,14 @@ def to_string( return cast( str, config.etree.tostring( # type: ignore[attr-defined] - self.etree_element(), encoding="UTF-8", + self.etree_element(), + encoding="UTF-8", ).decode("UTF-8"), ) @classmethod def _get_ns(cls, ns: Optional[str]) -> str: - return cls._namespaces[0] if ns is None else ns + return cls._default_ns if ns is None else ns @classmethod def _get_kwargs( @@ -202,8 +202,7 @@ class _BaseObject(_XMLObject): mechanism is to be used. """ - _namespace = config.KMLNS - _namespaces: Tuple[str, ...] = (config.KMLNS,) + _default_ns = config.KMLNS id = None target_id = None diff --git a/fastkml/data.py b/fastkml/data.py index ac553e2e..491649c6 100644 --- a/fastkml/data.py +++ b/fastkml/data.py @@ -14,7 +14,7 @@ # along with this library; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA """ -Add Custom Data +Add Custom Data. https://developers.google.com/kml/documentation/extendeddata#example """ @@ -98,7 +98,8 @@ def __init__( fields: Optional[Iterable[SimpleField]] = None, ) -> None: if id is None: - raise KMLSchemaError("Id is required for schema") + msg = "Id is required for schema" + raise KMLSchemaError(msg) super().__init__(ns=ns, id=id, target_id=target_id) self.name = name self._simple_fields = list(fields) if fields else [] @@ -136,13 +137,15 @@ def etree_element( element.set("name", self.name) for simple_field in self.simple_fields: sf = config.etree.SubElement( # type: ignore[attr-defined] - element, f"{self.ns}SimpleField", + element, + f"{self.ns}SimpleField", ) sf.set("type", simple_field.type.value) sf.set("name", simple_field.name) if simple_field.display_name: dn = config.etree.SubElement( # type: ignore[attr-defined] - sf, f"{self.ns}displayName", + sf, + f"{self.ns}displayName", ) dn.text = simple_field.display_name return element @@ -180,7 +183,9 @@ def _get_kwargs( kwargs = super()._get_kwargs(ns=ns, element=element, strict=strict) kwargs["name"] = element.get("name") kwargs["fields"] = cls._get_fields_kwargs_from_element( - ns=ns, element=element, strict=strict, + ns=ns, + element=element, + strict=strict, ) return kwargs @@ -219,12 +224,14 @@ def etree_element( element = super().etree_element(precision=precision, verbosity=verbosity) element.set("name", self.name or "") value = config.etree.SubElement( # type: ignore[attr-defined] - element, f"{self.ns}value", + element, + f"{self.ns}value", ) value.text = self.value if self.display_name: display_name = config.etree.SubElement( # type: ignore[attr-defined] - element, f"{self.ns}displayName", + element, + f"{self.ns}displayName", ) display_name.text = self.display_name return element @@ -277,7 +284,8 @@ def __init__( ) -> None: super().__init__(ns) if (not isinstance(schema_url, str)) or (not schema_url): - raise ValueError("required parameter schema_url missing") + msg = "required parameter schema_url missing" + raise ValueError(msg) self.schema_url = schema_url self._data = list(data) if data else [] @@ -309,7 +317,8 @@ def etree_element( element.set("schemaUrl", self.schema_url) for data in self.data: sd = config.etree.SubElement( # type: ignore[attr-defined] - element, f"{self.ns}SimpleData", + element, + f"{self.ns}SimpleData", ) sd.set("name", data.name) sd.text = data.value @@ -386,7 +395,9 @@ def _get_kwargs( typed_data = element.findall(f"{ns}SchemaData") for sd in typed_data: el_schema_data = SchemaData.class_from_element( - ns=ns, element=sd, strict=strict, + ns=ns, + element=sd, + strict=strict, ) elements.append(el_schema_data) kwargs["elements"] = elements diff --git a/fastkml/enums.py b/fastkml/enums.py index 431d7660..c57d0807 100644 --- a/fastkml/enums.py +++ b/fastkml/enums.py @@ -20,8 +20,17 @@ __all__ = ["AltitudeMode", "DateTimeResolution", "Verbosity"] +class REnum(Enum): + """Enum with custom repr for eval roundtrip.""" + + def __repr__(self) -> str: + """The string representation of the object.""" + cls_name = self.__class__.__name__ + return f"{cls_name}.{self.name}" + + @unique -class Verbosity(Enum): +class Verbosity(REnum): """Enum to represent the different verbosity levels.""" quiet = 0 @@ -30,7 +39,7 @@ class Verbosity(Enum): @unique -class DateTimeResolution(Enum): +class DateTimeResolution(REnum): """Enum to represent the different date time resolutions.""" datetime = "dateTime" @@ -40,7 +49,7 @@ class DateTimeResolution(Enum): @unique -class AltitudeMode(Enum): +class AltitudeMode(REnum): """ Enum to represent the different altitude modes. @@ -88,7 +97,7 @@ class AltitudeMode(Enum): @unique -class DataType(Enum): +class DataType(REnum): string = "string" int_ = "int" uint = "uint" @@ -108,4 +117,5 @@ def convert(self, value: str) -> Union[str, int, float, bool]: return float(value) if self == DataType.bool_: return bool(int(value)) - raise ValueError(f"Unknown data type {self}") + msg = f"Unknown data type {self}" + raise ValueError(msg) diff --git a/fastkml/geometry.py b/fastkml/geometry.py index 6bcffe25..84176b04 100644 --- a/fastkml/geometry.py +++ b/fastkml/geometry.py @@ -53,7 +53,10 @@ GeometryType = Union[geo.Polygon, geo.LineString, geo.LinearRing, geo.Point] MultiGeometryType = Union[ - geo.MultiPoint, geo.MultiLineString, geo.MultiPolygon, geo.GeometryCollection, + geo.MultiPoint, + geo.MultiLineString, + geo.MultiPolygon, + geo.GeometryCollection, ] AnyGeometryType = Union[GeometryType, MultiGeometryType] @@ -150,18 +153,18 @@ def _etree_coordinates( if len(coordinates[0]) == 2: tuples = (f"{c[0]:f},{c[1]:f}" for c in coordinates) elif len(coordinates[0]) == 3: - tuples = ( - f"{c[0]:f},{c[1]:f},{c[2]:f}" for c in coordinates # type: ignore[misc] - ) + tuples = (f"{c[0]:f},{c[1]:f},{c[2]:f}" for c in coordinates) else: - raise KMLWriteError(f"Invalid dimensions in coordinates '{coordinates}'") + msg = f"Invalid dimensions in coordinates '{coordinates}'" + raise KMLWriteError(msg) element.text = " ".join(tuples) return element def _set_altitude_mode(self, element: Element) -> None: if self.altitude_mode: am_element = config.etree.SubElement( # type: ignore[attr-defined] - element, f"{self.ns}altitudeMode", + element, + f"{self.ns}altitudeMode", ) am_element.text = self.altitude_mode.value @@ -170,7 +173,8 @@ def _set_extrude(self, element: Element) -> None: et_element = cast( Element, config.etree.SubElement( # type: ignore[attr-defined] - element, f"{self.ns}extrude", + element, + f"{self.ns}extrude", ), ) et_element.text = str(int(self.extrude)) @@ -180,7 +184,8 @@ def _set_tessellate(self, element: Element) -> None: t_element = cast( Element, config.etree.SubElement( # type: ignore[attr-defined] - element, f"{self.ns}tessellate", + element, + f"{self.ns}tessellate", ), ) t_element.text = str(int(self.tessellate)) @@ -199,7 +204,11 @@ def etree_element( @classmethod def _get_coordinates( - cls, *, ns: str, element: Element, strict: bool, + cls, + *, + ns: str, + element: Element, + strict: bool, ) -> List[PointType]: """ Get coordinates from element. @@ -281,7 +290,9 @@ def _get_geometry_kwargs( "extrude": cls._get_extrude(ns=ns, element=element, strict=strict), "tessellate": cls._get_tessellate(ns=ns, element=element, strict=strict), "altitude_mode": cls._get_altitude_mode( - ns=ns, element=element, strict=strict, + ns=ns, + element=element, + strict=strict, ), } @@ -361,7 +372,8 @@ def _get_geometry( element, encoding="UTF-8", ).decode("UTF-8") - raise KMLParseError(f"Invalid coordinates in {error}") from e + msg = f"Invalid coordinates in {error}" + raise KMLParseError(msg) from e class LineString(_Geometry): @@ -414,7 +426,8 @@ def _get_geometry( element, encoding="UTF-8", ).decode("UTF-8") - raise KMLParseError(f"Invalid coordinates in {error}") from e + msg = f"Invalid coordinates in {error}" + raise KMLParseError(msg) from e class LinearRing(LineString): @@ -455,7 +468,8 @@ def _get_geometry( element, encoding="UTF-8", ).decode("UTF-8") - raise KMLParseError(f"Invalid coordinates in {error}") from e + msg = f"Invalid coordinates in {error}" + raise KMLParseError(msg) from e class Polygon(_Geometry): @@ -498,7 +512,8 @@ def etree_element( ) outer_boundary.append( linear_ring(geometry=self.geometry.exterior).etree_element( - precision=precision, verbosity=verbosity, + precision=precision, + verbosity=verbosity, ), ) for interior in self.geometry.interiors: @@ -511,7 +526,8 @@ def etree_element( ) inner_boundary.append( linear_ring(geometry=interior).etree_element( - precision=precision, verbosity=verbosity, + precision=precision, + verbosity=verbosity, ), ) return element @@ -524,14 +540,16 @@ def _get_geometry(cls, *, ns: str, element: Element, strict: bool) -> geo.Polygo element, encoding="UTF-8", ).decode("UTF-8") - raise KMLParseError(f"Missing outerBoundaryIs in {error}") + msg = f"Missing outerBoundaryIs in {error}" + raise KMLParseError(msg) outer_ring = outer_boundary.find(f"{ns}LinearRing") if outer_ring is None: error = config.etree.tostring( # type: ignore[attr-defined] element, encoding="UTF-8", ).decode("UTF-8") - raise KMLParseError(f"Missing LinearRing in {error}") + msg = f"Missing LinearRing in {error}" + raise KMLParseError(msg) exterior = LinearRing._get_geometry(ns=ns, element=outer_ring, strict=strict) interiors = [] for inner_boundary in element.findall(f"{ns}innerBoundaryIs"): @@ -541,7 +559,8 @@ def _get_geometry(cls, *, ns: str, element: Element, strict: bool) -> geo.Polygo element, encoding="UTF-8", ).decode("UTF-8") - raise KMLParseError(f"Missing LinearRing in {error}") + msg = f"Missing LinearRing in {error}" + raise KMLParseError(msg) interiors.append( LinearRing._get_geometry(ns=ns, element=inner_ring, strict=strict), ) @@ -575,7 +594,7 @@ def create_multigeometry( } for geometry_name, constructor in map_to_geometries.items(): if geom_type == geometry_name: - return constructor( # type: ignore[operator, no-any-return] + return constructor( *geometries, ) @@ -637,20 +656,28 @@ def etree_element( extrude=None, tessellate=None, altitude_mode=None, - geometry=geometry, # type: ignore[arg-type] + geometry=geometry, ).etree_element(precision=precision, verbosity=verbosity), ) return element @classmethod def _get_geometry( - cls, *, ns: str, element: Element, strict: bool, + cls, + *, + ns: str, + element: Element, + strict: bool, ) -> Optional[MultiGeometryType]: geometries = [] - allowed_geometries = (cls,) + tuple(cls.map_to_kml.values()) + allowed_geometries = (cls, *tuple(cls.map_to_kml.values())) for g in allowed_geometries: for e in element.findall(f"{ns}{g.__name__}"): - geometry = g._get_geometry(ns=ns, element=e, strict=strict) + geometry = g._get_geometry( # type: ignore[attr-defined] + ns=ns, + element=e, + strict=strict, + ) if geometry is not None: geometries.append(geometry) return create_multigeometry(geometries) diff --git a/fastkml/gx.py b/fastkml/gx.py index 432f9da1..9dbb8780 100644 --- a/fastkml/gx.py +++ b/fastkml/gx.py @@ -202,7 +202,8 @@ def __init__( track_items: Optional[Sequence[TrackItem]] = None, ) -> None: if geometry and track_items: - raise ValueError("Cannot specify both geometry and track_items") + msg = "Cannot specify both geometry and track_items" + raise ValueError(msg) if geometry: track_items = linestring_to_track_items(geometry) elif track_items: @@ -242,7 +243,9 @@ def etree_element( if self.track_items: for track_item in self.track_items: for track_item_element in track_item.etree_elements( - precision=precision, verbosity=verbosity, name_spaces=name_spaces, + precision=precision, + verbosity=verbosity, + name_spaces=name_spaces, ): element.append(track_item_element) return element @@ -290,13 +293,16 @@ def _get_kwargs( ) -> Dict[str, Any]: kwargs = super()._get_kwargs(ns=ns, element=element, strict=strict) kwargs["track_items"] = cls.track_items_kwargs_from_element( - ns=ns, element=element, strict=strict, + ns=ns, + element=element, + strict=strict, ) return kwargs def multilinestring_to_tracks( - multilinestring: geo.MultiLineString, ns: Optional[str], + multilinestring: geo.MultiLineString, + ns: Optional[str], ) -> List[Track]: return [Track(ns=ns, geometry=linestring) for linestring in multilinestring.geoms] @@ -322,7 +328,8 @@ def __init__( interpolate: Optional[bool] = None, ) -> None: if geometry and tracks: - raise ValueError("Cannot specify both geometry and track_items") + msg = "Cannot specify both geometry and track_items" + raise ValueError(msg) if geometry: tracks = multilinestring_to_tracks(geometry, ns=ns) elif tracks: @@ -365,14 +372,17 @@ def etree_element( i_element = cast( Element, config.etree.SubElement( # type: ignore[attr-defined] - element, f"{self.ns}interpolate", + element, + f"{self.ns}interpolate", ), ) i_element.text = str(int(self.interpolate)) for track in self.tracks or []: element.append( track.etree_element( - precision=precision, verbosity=verbosity, name_spaces=name_spaces, + precision=precision, + verbosity=verbosity, + name_spaces=name_spaces, ), ) return element @@ -423,9 +433,13 @@ def _get_kwargs( ) -> Dict[str, Any]: kwargs = super()._get_kwargs(ns=ns, element=element, strict=strict) kwargs["interpolate"] = cls._get_interpolate( - ns=ns, element=element, strict=strict, + ns=ns, + element=element, + strict=strict, ) kwargs["tracks"] = cls._get_track_kwargs_from_element( - ns=config.GXNS, element=element, strict=strict, + ns=config.GXNS, + element=element, + strict=strict, ) return kwargs diff --git a/fastkml/kml.py b/fastkml/kml.py index 893ffa5b..8978e29c 100644 --- a/fastkml/kml.py +++ b/fastkml/kml.py @@ -26,6 +26,8 @@ """ import logging import urllib.parse as urlparse +from datetime import datetime +from typing import Any from typing import Iterator from typing import List from typing import Optional @@ -76,7 +78,7 @@ class _Feature(TimeMixin, _BaseObject): * Placemark * Overlay Not Implemented Yet: - * NetworkLink + * NetworkLink. """ name = None @@ -201,14 +203,15 @@ def __init__( def style_url(self) -> Optional[str]: """ Returns the url only, not a full StyleUrl object. - if you need the full StyleUrl object use _style_url + if you need the full StyleUrl object use _style_url. """ if isinstance(self._style_url, StyleUrl): return self._style_url.url + return None @style_url.setter def style_url(self, styleurl: Union[str, StyleUrl, None]) -> None: - """You may pass a StyleUrl Object, a string or None""" + """You may pass a StyleUrl Object, a string or None.""" if isinstance(styleurl, StyleUrl): self._style_url = styleurl elif isinstance(styleurl, str): @@ -224,16 +227,16 @@ def camera(self): return self._camera @camera.setter - def camera(self, camera): + def camera(self, camera) -> None: if isinstance(camera, Camera): self._camera = camera @property - def look_at(self): + def look_at(self) -> datetime: return self._look_at @look_at.setter - def look_at(self, look_at): + def look_at(self, look_at) -> None: if isinstance(look_at, LookAt): self._look_at = look_at @@ -242,7 +245,7 @@ def link(self): return self._atom_link.href @link.setter - def link(self, url): + def link(self, url) -> None: if isinstance(url, str): self._atom_link = atom.Link(href=url) elif isinstance(url, atom.Link): @@ -253,12 +256,13 @@ def link(self, url): raise TypeError @property - def author(self): + def author(self) -> None: if self._atom_author: return self._atom_author.name + return None @author.setter - def author(self, name): + def author(self, name) -> None: if isinstance(name, atom.Author): self._atom_author = name elif isinstance(name, str): @@ -272,14 +276,14 @@ def author(self, name): raise TypeError def append_style(self, style: Union[Style, StyleMap]) -> None: - """Append a style to the feature""" + """Append a style to the feature.""" if isinstance(style, _StyleSelector): self._styles.append(style) else: raise TypeError def styles(self) -> Iterator[Union[Style, StyleMap]]: - """Iterate over the styles of this feature""" + """Iterate over the styles of this feature.""" for style in self._styles: if isinstance(style, _StyleSelector): yield style @@ -287,7 +291,7 @@ def styles(self) -> Iterator[Union[Style, StyleMap]]: raise TypeError @property - def snippet(self): + def snippet(self) -> dict | None | dict[str, Any]: if not self._snippet: return None if isinstance(self._snippet, dict): @@ -300,16 +304,18 @@ def snippet(self): elif int(max_lines) > 0: # if maxLines <=0 ignore it return {"text": text, "maxLines": max_lines} + return None + return None elif isinstance(self._snippet, str): return self._snippet else: + msg = "Snippet must be dict of {'text':t, 'maxLines':i} or string" raise ValueError( - "Snippet must be dict of " - "{'text':t, 'maxLines':i} or string", + msg, ) @snippet.setter - def snippet(self, snip=None): + def snippet(self, snip=None) -> None: self._snippet = {} if isinstance(snip, dict): self._snippet["text"] = snip.get("text") @@ -321,18 +327,19 @@ def snippet(self, snip=None): elif snip is None: self._snippet = None else: + msg = "Snippet must be dict of {'text':t, 'maxLines':i} or string" raise ValueError( - "Snippet must be dict of " - "{'text':t, 'maxLines':i} or string", + msg, ) @property - def address(self): + def address(self) -> None: if self._address: return self._address + return None @address.setter - def address(self, address): + def address(self, address) -> None: if isinstance(address, str): self._address = address elif address is None: @@ -341,12 +348,13 @@ def address(self, address): raise ValueError @property - def phone_number(self): + def phone_number(self) -> None: if self._phone_number: return self._phone_number + return None @phone_number.setter - def phone_number(self, phone_number): + def phone_number(self, phone_number) -> None: if isinstance(phone_number, str): self._phone_number = phone_number elif phone_number is None: @@ -367,7 +375,8 @@ def etree_element( description = config.etree.SubElement(element, f"{self.ns}description") description.text = self.description if (self.camera is not None) and (self.look_at is not None): - raise ValueError("Either Camera or LookAt can be defined, not both") + msg = "Either Camera or LookAt can be defined, not both" + raise ValueError(msg) if self.camera is not None: element.append(self._camera.etree_element()) elif self.look_at is not None: @@ -391,7 +400,8 @@ def etree_element( if self.snippet.get("maxLines"): snippet.set("maxLines", str(self.snippet["maxLines"])) if (self._timespan is not None) and (self._timestamp is not None): - raise ValueError("Either Timestamp or Timespan can be defined, not both") + msg = "Either Timestamp or Timespan can be defined, not both" + raise ValueError(msg) elif self._timespan is not None: element.append(self._timespan.etree_element()) elif self._timestamp is not None: @@ -602,7 +612,7 @@ def view_refresh_mode(self): return self._view_refresh_mode @view_refresh_mode.setter - def view_refresh_mode(self, view_refresh_mode): + def view_refresh_mode(self, view_refresh_mode) -> None: if isinstance(view_refresh_mode, str): self._view_refresh_mode = view_refresh_mode elif view_refresh_mode is None: @@ -619,7 +629,7 @@ def view_refresh_time(self): return self._view_refresh_time @view_refresh_time.setter - def view_refresh_time(self, view_refresh_time: Optional[float]): + def view_refresh_time(self, view_refresh_time: Optional[float]) -> None: if isinstance(view_refresh_time, float): self._view_refresh_time = view_refresh_time elif view_refresh_time is None: @@ -652,7 +662,7 @@ def view_format(self): """ Specifies the format of the query string that is appended to the Link's before the file is fetched. - (If the specifies a local file, this element is ignored.) + (If the specifies a local file, this element is ignored.). This information matches the Web Map Service (WMS) bounding box specification. If you specify an empty tag, no information is appended to the @@ -679,7 +689,7 @@ def view_format(self): return self._view_format @view_format.setter - def view_format(self, view_format): + def view_format(self, view_format) -> None: if isinstance(view_format, str): self._view_format = view_format elif view_format is None: @@ -701,7 +711,7 @@ def http_query(self): return self._http_query @http_query.setter - def http_query(self, http_query): + def http_query(self, http_query) -> None: if isinstance(http_query, str): self._http_query = http_query elif http_query is None: @@ -724,22 +734,26 @@ def etree_element( refresh_mode.text = self._refresh_mode if self._refresh_interval: refresh_interval = config.etree.SubElement( - element, f"{self.ns}refreshInterval", + element, + f"{self.ns}refreshInterval", ) refresh_interval.text = str(self._refresh_interval) if self._view_refresh_mode: view_refresh_mode = config.etree.SubElement( - element, f"{self.ns}viewRefreshMode", + element, + f"{self.ns}viewRefreshMode", ) view_refresh_mode.text = self._view_refresh_mode if self._view_refresh_time: view_refresh_time = config.etree.SubElement( - element, f"{self.ns}viewRefreshTime", + element, + f"{self.ns}viewRefreshTime", ) view_refresh_time.text = str(self._view_refresh_time) if self._view_bound_scale: view_bound_scale = config.etree.SubElement( - element, f"{self.ns}viewBoundScale", + element, + f"{self.ns}viewBoundScale", ) view_bound_scale.text = str(self._view_bound_scale) if self._view_format: @@ -803,7 +817,7 @@ class _Container(_Feature): creation of nested hierarchies. subclasses are: Document, - Folder + Folder. """ _features = [] @@ -831,14 +845,17 @@ def __init__( self._features = features or [] def features(self) -> Iterator[_Feature]: - """Iterate over features""" + """Iterate over features.""" for feature in self._features: if isinstance(feature, (Folder, Placemark, Document, _Overlay)): yield feature else: - raise TypeError( + msg = ( "Features must be instances of " - "(Folder, Placemark, Document, Overlay)", + "(Folder, Placemark, Document, Overlay)" + ) + raise TypeError( + msg, ) def etree_element( @@ -852,21 +869,22 @@ def etree_element( return element def append(self, kmlobj: _Feature) -> None: - """Append a feature""" + """Append a feature.""" if id(kmlobj) == id(self): - raise ValueError("Cannot append self") + msg = "Cannot append self" + raise ValueError(msg) if isinstance(kmlobj, (Folder, Placemark, Document, _Overlay)): self._features.append(kmlobj) else: + msg = "Features must be instances of (Folder, Placemark, Document, Overlay)" raise TypeError( - "Features must be instances of " - "(Folder, Placemark, Document, Overlay)", + msg, ) class _Overlay(_Feature): """ - abstract element; do not create + abstract element; do not create. Base type for image overlays drawn on the planet surface or on the screen @@ -919,7 +937,7 @@ def color(self): return self._color @color.setter - def color(self, color): + def color(self, color) -> None: if isinstance(color, str): self._color = color elif color is None: @@ -928,11 +946,11 @@ def color(self, color): raise ValueError @property - def draw_order(self): + def draw_order(self) -> str | None: return self._draw_order @draw_order.setter - def draw_order(self, value): + def draw_order(self, value) -> None: if isinstance(value, (str, int, float)): self._draw_order = str(value) elif value is None: @@ -941,11 +959,11 @@ def draw_order(self, value): raise ValueError @property - def icon(self): + def icon(self) -> Icon | None: return self._icon @icon.setter - def icon(self, value): + def icon(self, value) -> None: if isinstance(value, Icon): self._icon = value elif value is None: @@ -1075,11 +1093,11 @@ class PhotoOverlay(_Overlay): # for spherical panoramas @property - def rotation(self): + def rotation(self) -> str | None: return self._rotation @rotation.setter - def rotation(self, value): + def rotation(self, value) -> None: if isinstance(value, (str, int, float)): self._rotation = str(value) elif value is None: @@ -1088,11 +1106,11 @@ def rotation(self, value): raise ValueError @property - def left_fov(self): + def left_fov(self) -> str | None: return self._left_fow @left_fov.setter - def left_fov(self, value): + def left_fov(self, value) -> None: if isinstance(value, (str, int, float)): self._left_fow = str(value) elif value is None: @@ -1101,11 +1119,11 @@ def left_fov(self, value): raise ValueError @property - def right_fov(self): + def right_fov(self) -> str | None: return self._right_fov @right_fov.setter - def right_fov(self, value): + def right_fov(self, value) -> None: if isinstance(value, (str, int, float)): self._right_fov = str(value) elif value is None: @@ -1114,11 +1132,11 @@ def right_fov(self, value): raise ValueError @property - def bottom_fov(self): + def bottom_fov(self) -> str | None: return self._bottom_fov @bottom_fov.setter - def bottom_fov(self, value): + def bottom_fov(self, value) -> None: if isinstance(value, (str, int, float)): self._bottom_fov = str(value) elif value is None: @@ -1127,11 +1145,11 @@ def bottom_fov(self, value): raise ValueError @property - def top_fov(self): + def top_fov(self) -> str | None: return self._top_fov @top_fov.setter - def top_fov(self, value): + def top_fov(self, value) -> None: if isinstance(value, (str, int, float)): self._top_fov = str(value) elif value is None: @@ -1140,11 +1158,11 @@ def top_fov(self, value): raise ValueError @property - def near(self): + def near(self) -> str | None: return self._near @near.setter - def near(self, value): + def near(self, value) -> None: if isinstance(value, (str, int, float)): self._near = str(value) elif value is None: @@ -1153,11 +1171,11 @@ def near(self, value): raise ValueError @property - def tile_size(self): + def tile_size(self) -> str | None: return self._tile_size @tile_size.setter - def tile_size(self, value): + def tile_size(self, value) -> None: if isinstance(value, (str, int, float)): self._tile_size = str(value) elif value is None: @@ -1166,11 +1184,11 @@ def tile_size(self, value): raise ValueError @property - def max_width(self): + def max_width(self) -> str | None: return self._max_width @max_width.setter - def max_width(self, value): + def max_width(self, value) -> None: if isinstance(value, (str, int, float)): self._max_width = str(value) elif value is None: @@ -1179,11 +1197,11 @@ def max_width(self, value): raise ValueError @property - def max_height(self): + def max_height(self) -> str | None: return self._max_height @max_height.setter - def max_height(self, value): + def max_height(self, value) -> None: if isinstance(value, (str, int, float)): self._max_height = str(value) elif value is None: @@ -1192,11 +1210,11 @@ def max_height(self, value): raise ValueError @property - def grid_origin(self): + def grid_origin(self) -> str | None: return self._grid_origin @grid_origin.setter - def grid_origin(self, value): + def grid_origin(self, value) -> None: if value in ("lowerLeft", "upperLeft"): self._grid_origin = str(value) elif value is None: @@ -1205,37 +1223,38 @@ def grid_origin(self, value): raise ValueError @property - def point(self): + def point(self) -> str: return self._point @point.setter - def point(self, value): + def point(self, value) -> None: if isinstance(value, (str, tuple)): self._point = str(value) else: raise ValueError @property - def shape(self): + def shape(self) -> str | None: return self._shape @shape.setter - def shape(self, value): + def shape(self, value) -> None: if value in ("rectangle", "cylinder", "sphere"): self._shape = str(value) elif value is None: self._shape = None else: - raise ValueError("Shape must be one of rectangle, cylinder, sphere") + msg = "Shape must be one of rectangle, cylinder, sphere" + raise ValueError(msg) - def view_volume(self, left_fov, right_fov, bottom_fov, top_fov, near): + def view_volume(self, left_fov, right_fov, bottom_fov, top_fov, near) -> None: self.left_fov = left_fov self.right_fov = right_fov self.bottom_fov = bottom_fov self.top_fov = top_fov self.near = near - def image_pyramid(self, tile_size, max_width, max_height, grid_origin): + def image_pyramid(self, tile_size, max_width, max_height, grid_origin) -> None: self.tile_size = tile_size self.max_width = max_width self.max_height = max_height @@ -1245,7 +1264,7 @@ def etree_element( self, precision: Optional[int] = None, verbosity: Verbosity = Verbosity.normal, - ): + ) -> Element: element = super().etree_element(precision=precision, verbosity=verbosity) if self._rotation: rotation = config.etree.SubElement(element, f"{self.ns}rotation") @@ -1282,7 +1301,7 @@ def etree_element( grid_origin.text = self._grid_origin return element - def from_element(self, element): + def from_element(self, element) -> None: super().from_element(element) rotation = element.find(f"{self.ns}rotation") if rotation is not None: @@ -1389,11 +1408,11 @@ class GroundOverlay(_Overlay): _lat_lon_quad = None @property - def altitude(self): + def altitude(self) -> str | None: return self._altitude @altitude.setter - def altitude(self, value): + def altitude(self, value) -> None: if isinstance(value, (str, int, float)): self._altitude = str(value) elif value is None: @@ -1402,22 +1421,22 @@ def altitude(self, value): raise ValueError @property - def altitude_mode(self): + def altitude_mode(self) -> str: return self._altitude_mode @altitude_mode.setter - def altitude_mode(self, mode): + def altitude_mode(self, mode) -> None: if mode in ("clampToGround", "absolute"): self._altitude_mode = str(mode) else: self._altitude_mode = "clampToGround" @property - def north(self): + def north(self) -> str | None: return self._north @north.setter - def north(self, value): + def north(self, value) -> None: if isinstance(value, (str, int, float)): self._north = str(value) elif value is None: @@ -1426,11 +1445,11 @@ def north(self, value): raise ValueError @property - def south(self): + def south(self) -> str | None: return self._south @south.setter - def south(self, value): + def south(self, value) -> None: if isinstance(value, (str, int, float)): self._south = str(value) elif value is None: @@ -1439,11 +1458,11 @@ def south(self, value): raise ValueError @property - def east(self): + def east(self) -> str | None: return self._east @east.setter - def east(self, value): + def east(self, value) -> None: if isinstance(value, (str, int, float)): self._east = str(value) elif value is None: @@ -1452,11 +1471,11 @@ def east(self, value): raise ValueError @property - def west(self): + def west(self) -> str | None: return self._west @west.setter - def west(self, value): + def west(self, value) -> None: if isinstance(value, (str, int, float)): self._west = str(value) elif value is None: @@ -1465,11 +1484,11 @@ def west(self, value): raise ValueError @property - def rotation(self): + def rotation(self) -> str | None: return self._rotation @rotation.setter - def rotation(self, value): + def rotation(self, value) -> None: if isinstance(value, (str, int, float)): self._rotation = str(value) elif value is None: @@ -1478,7 +1497,12 @@ def rotation(self, value): raise ValueError def lat_lon_box( - self, north: int, south: int, east: int, west: int, rotation: int = 0, + self, + north: int, + south: int, + east: int, + west: int, + rotation: int = 0, ) -> None: if -90 <= float(north) <= 90: self.north = north @@ -1512,7 +1536,8 @@ def etree_element( altitude.text = self._altitude if self._altitude_mode: altitude_mode = config.etree.SubElement( - element, f"{self.ns}altitudeMode", + element, + f"{self.ns}altitudeMode", ) altitude_mode.text = self._altitude_mode if all([self._north, self._south, self._east, self._west]): @@ -1561,7 +1586,7 @@ class Document(_Container): """ A Document is a container for features and styles. This element is required if your KML file uses shared styles or schemata for typed - extended data + extended data. """ __name__ = "Document" @@ -1618,6 +1643,7 @@ def get_style_by_url(self, style_url: str) -> Union[Style, StyleMap]: for style in self.styles(): if style.id == id: return style + return None class Folder(_Container): @@ -1748,7 +1774,6 @@ def from_element(self, element: Element, strict=False) -> None: return logger.warning("No geometries found") logger.debug("Problem with element: %", config.etree.tostring(element)) - # raise ValueError('No geometries found') def etree_element( self, @@ -1764,7 +1789,7 @@ def etree_element( class KML: - """represents a KML File""" + """represents a KML File.""" _features = [] ns = None @@ -1781,10 +1806,11 @@ def __init__(self, ns: Optional[str] = None) -> None: self.ns = config.KMLNS if ns is None else ns def from_string(self, xml_string: str) -> None: - """Create a KML object from a xml string""" + """Create a KML object from a xml string.""" try: element = config.etree.fromstring( - xml_string, parser=config.etree.XMLParser(huge_tree=True, recover=True), + xml_string, + parser=config.etree.XMLParser(huge_tree=True, recover=True), ) except TypeError: element = config.etree.XML(xml_string) @@ -1833,7 +1859,8 @@ def etree_element( else: try: root = config.etree.Element( - f"{self.ns}kml", nsmap={None: self.ns[1:-1]}, + f"{self.ns}kml", + nsmap={None: self.ns[1:-1]}, ) except TypeError: root = config.etree.Element(f"{self.ns}kml") @@ -1842,7 +1869,7 @@ def etree_element( return root def to_string(self, prettyprint: bool = False) -> str: - """Return the KML Object as serialized xml""" + """Return the KML Object as serialized xml.""" try: return config.etree.tostring( self.etree_element(), @@ -1855,25 +1882,28 @@ def to_string(self, prettyprint: bool = False) -> str: ) def features(self) -> Iterator[Union[Folder, Document, Placemark]]: - """Iterate over features""" + """Iterate over features.""" for feature in self._features: if isinstance(feature, (Document, Folder, Placemark, _Overlay)): yield feature else: - raise TypeError( + msg = ( "Features must be instances of " - "(Document, Folder, Placemark, Overlay)", + "(Document, Folder, Placemark, Overlay)" ) + raise TypeError(msg) def append(self, kmlobj: Union[Folder, Document, Placemark]) -> None: - """Append a feature""" + """Append a feature.""" if id(kmlobj) == id(self): - raise ValueError("Cannot append self") + msg = "Cannot append self" + raise ValueError(msg) if isinstance(kmlobj, (Document, Folder, Placemark, _Overlay)): self._features.append(kmlobj) else: + msg = "Features must be instances of (Document, Folder, Placemark, Overlay)" raise TypeError( - "Features must be instances of (Document, Folder, Placemark, Overlay)", + msg, ) diff --git a/fastkml/mixins.py b/fastkml/mixins.py index 60b56a12..cfc6adc7 100644 --- a/fastkml/mixins.py +++ b/fastkml/mixins.py @@ -30,7 +30,7 @@ class TimeMixin: @property def time_stamp(self) -> Optional[KmlDateTime]: - """This just returns the datetime portion of the timestamp""" + """This just returns the datetime portion of the timestamp.""" return self._timestamp.timestamp if self._timestamp is not None else None @time_stamp.setter diff --git a/fastkml/styles.py b/fastkml/styles.py index 8698c019..1bc551fb 100644 --- a/fastkml/styles.py +++ b/fastkml/styles.py @@ -91,7 +91,7 @@ class _ColorStyle(_BaseObject): This is an abstract element and cannot be used directly in a KML file. It provides elements for specifying the color and color mode of extended style types. - subclasses are: IconStyle, LabelStyle, LineStyle, PolyStyle + subclasses are: IconStyle, LabelStyle, LineStyle, PolyStyle. """ id = None @@ -156,7 +156,7 @@ class HotSpot(TypedDict): class IconStyle(_ColorStyle): - """Specifies how icons for point Placemarks are drawn""" + """Specifies how icons for point Placemarks are drawn.""" __name__ = "IconStyle" scale = 1.0 @@ -181,7 +181,11 @@ def __init__( hot_spot: Optional[HotSpot] = None, ) -> None: super().__init__( - ns=ns, id=id, target_id=target_id, color=color, color_mode=color_mode, + ns=ns, + id=id, + target_id=target_id, + color=color, + color_mode=color_mode, ) self.scale = scale @@ -219,7 +223,8 @@ def etree_element( href.text = self.icon_href if self.hot_spot: hot_spot = config.etree.SubElement( # type: ignore[attr-defined] - element, f"{self.ns}hotSpot", + element, + f"{self.ns}hotSpot", ) hot_spot.attrib["x"] = str(self.hot_spot["x"]) hot_spot.attrib["y"] = str(self.hot_spot["y"]) @@ -272,7 +277,11 @@ def __init__( width: Union[int, float] = 1, ) -> None: super().__init__( - ns=ns, id=id, target_id=target_id, color=color, color_mode=color_mode, + ns=ns, + id=id, + target_id=target_id, + color=color, + color_mode=color_mode, ) self.width = width @@ -322,7 +331,11 @@ def __init__( outline: int = 1, ) -> None: super().__init__( - ns=ns, id=id, target_id=target_id, color=color, color_mode=color_mode, + ns=ns, + id=id, + target_id=target_id, + color=color, + color_mode=color_mode, ) self.fill = fill self.outline = outline @@ -366,9 +379,7 @@ def strtobool(val: str) -> int: class LabelStyle(_ColorStyle): - """ - Specifies how the of a Feature is drawn in the 3D viewer. - """ + """Specifies how the of a Feature is drawn in the 3D viewer.""" __name__ = "LabelStyle" scale = 1.0 @@ -384,7 +395,11 @@ def __init__( scale: float = 1.0, ) -> None: super().__init__( - ns=ns, id=id, target_id=target_id, color=color, color_mode=color_mode, + ns=ns, + id=id, + target_id=target_id, + color=color, + color_mode=color_mode, ) self.scale = scale diff --git a/fastkml/times.py b/fastkml/times.py index 5f61cb92..57584dbb 100644 --- a/fastkml/times.py +++ b/fastkml/times.py @@ -74,7 +74,7 @@ def __init__( self, dt: Union[date, datetime], resolution: Optional[DateTimeResolution] = None, - ): + ) -> None: """Initialize a KmlDateTime object.""" self.dt = dt self.resolution = resolution @@ -171,7 +171,8 @@ def etree_element( ) -> Element: element = super().etree_element(precision=precision, verbosity=verbosity) when = config.etree.SubElement( # type: ignore[attr-defined] - element, f"{self.ns}when", + element, + f"{self.ns}when", ) when.text = str(self.timestamp) return element @@ -232,6 +233,7 @@ def etree_element( ) end.text = text if self.begin == self.end is None: - raise ValueError("Either begin, end or both must be set") + msg = "Either begin, end or both must be set" + raise ValueError(msg) # TODO test if end > begin return element diff --git a/fastkml/views.py b/fastkml/views.py index 8e9973f8..17b738a9 100644 --- a/fastkml/views.py +++ b/fastkml/views.py @@ -224,7 +224,8 @@ def etree_element( altitude_mode = config.etree.SubElement(element, f"{gx.NS}altitudeMode") altitude_mode.text = self.altitude_mode if (self._timespan is not None) and (self._timestamp is not None): - raise ValueError("Either Timestamp or Timespan can be defined, not both") + msg = "Either Timestamp or Timespan can be defined, not both" + raise ValueError(msg) if self._timespan is not None: element.append(self._timespan.etree_element()) elif self._timestamp is not None: diff --git a/pyproject.toml b/pyproject.toml index e57c7749..8e0f20b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -227,6 +227,14 @@ select = [ target-version = "py38" [tool.ruff.extend-per-file-ignores] +"tests/*.py" = [ + "D101", + "D102", + "D103", + "PLR2004", + "S101", + "SLF001", +] "tests/oldunit_test.py" = [ "E501", ] diff --git a/tests/data_test.py b/tests/data_test.py index 4b28b129..652b1a69 100644 --- a/tests/data_test.py +++ b/tests/data_test.py @@ -36,7 +36,9 @@ def test_schema(self) -> None: s = kml.Schema(ns, "some_id") assert not list(s.simple_fields) field = data.SimpleField( - name="Integer", type=DataType.int_, display_name="An Integer", + name="Integer", + type=DataType.int_, + display_name="An Integer", ) s.append(field) assert s.simple_fields[0] == field @@ -83,8 +85,8 @@ def test_schema_from_string(self) -> None: """ k = kml.KML() k.from_string(doc1) - d = list(k.features())[0] - s2 = list(d.schemata())[0] + d = next(iter(k.features())) + s2 = next(iter(d.schemata())) s.ns = config.KMLNS assert s.to_string() == s2.to_string() k1 = kml.KML() @@ -125,7 +127,10 @@ def test_untyped_extended_data(self) -> None: elements=[ data.Data(ns=ns, name="info", value="so much to see"), data.Data( - ns=ns, name="weather", display_name="Weather", value="blue skies", + ns=ns, + name="weather", + display_name="Weather", + value="blue skies", ), ], ) @@ -137,7 +142,7 @@ def test_untyped_extended_data(self) -> None: k2.from_string(k.to_string(prettyprint=True)) k.to_string() - extended_data = list(k2.features())[0].extended_data + extended_data = next(iter(k2.features())).extended_data assert extended_data is not None assert len(extended_data.elements), 2 assert extended_data.elements[0].name == "info" @@ -153,12 +158,14 @@ def test_untyped_extended_data_nested(self) -> None: d = kml.Document(ns, "docid", "doc name", "doc description") d.extended_data = kml.ExtendedData( - ns=ns, elements=[data.Data(ns=ns, name="type", value="Document")], + ns=ns, + elements=[data.Data(ns=ns, name="type", value="Document")], ) f = kml.Folder(ns, "fid", "f name", "f description") f.extended_data = kml.ExtendedData( - ns=ns, elements=[data.Data(ns=ns, name="type", value="Folder")], + ns=ns, + elements=[data.Data(ns=ns, name="type", value="Folder")], ) k.append(d) @@ -167,8 +174,8 @@ def test_untyped_extended_data_nested(self) -> None: k2 = kml.KML() k2.from_string(k.to_string()) - document_data = list(k2.features())[0].extended_data - folder_data = list(list(k2.features())[0].features())[0].extended_data + document_data = next(iter(k2.features())).extended_data + folder_data = next(iter(next(iter(k2.features())).features())).extended_data assert document_data.elements[0].name == "type" assert document_data.elements[0].value == "Document" @@ -209,7 +216,7 @@ def test_extended_data(self) -> None: k = kml.KML() k.from_string(doc) - extended_data = list(k.features())[0].extended_data + extended_data = next(iter(k.features())).extended_data assert extended_data.elements[0].name == "holeNumber" assert extended_data.elements[0].value == "1" @@ -222,7 +229,8 @@ def test_extended_data(self) -> None: ) sd = extended_data.elements[2] assert sd.data[0] == data.SimpleData( - name="TrailHeadName", value="Mount Everest", + name="TrailHeadName", + value="Mount Everest", ) assert sd.data[1] == data.SimpleData(name="TrailLength", value="347.45") assert sd.data[2] == data.SimpleData(name="ElevationGain", value="10000") diff --git a/tests/geometries/multigeometry_test.py b/tests/geometries/multigeometry_test.py index 10d279e4..67c2b2c8 100644 --- a/tests/geometries/multigeometry_test.py +++ b/tests/geometries/multigeometry_test.py @@ -25,7 +25,7 @@ class TestMultiPointStdLibrary(StdLibrary): """Test with the standard library.""" - def test_1_point(self): + def test_1_point(self) -> None: """Test with one point.""" p = geo.MultiPoint([(1, 2)]) @@ -35,7 +35,7 @@ def test_1_point(self): assert "MultiGeometry>" in mg.to_string() assert "Point>" in mg.to_string() - def test_2_points(self): + def test_2_points(self) -> None: """Test with two points.""" p = geo.MultiPoint([(1, 2), (3, 4)]) @@ -59,7 +59,7 @@ def test_2_points_read(self) -> None: class TestMultiLineStringStdLibrary(StdLibrary): - def test_1_linestring(self): + def test_1_linestring(self) -> None: """Test with one linestring.""" p = geo.MultiLineString([[(1, 2), (3, 4)]]) @@ -69,7 +69,7 @@ def test_1_linestring(self): assert "MultiGeometry>" in mg.to_string() assert "LineString>" in mg.to_string() - def test_2_linestrings(self): + def test_2_linestrings(self) -> None: """Test with two linestrings.""" p = geo.MultiLineString([[(1, 2), (3, 4)], [(5, 6), (7, 8)]]) @@ -95,7 +95,7 @@ def test_2_linestrings_read(self) -> None: class TestMultiPolygonStdLibrary(StdLibrary): - def test_1_polygon(self): + def test_1_polygon(self) -> None: """Test with one polygon.""" p = geo.MultiPolygon([[[[1, 2], [3, 4], [5, 6], [1, 2]]]]) @@ -110,7 +110,7 @@ def test_1_polygon(self): assert "outerBoundaryIs>" in mg.to_string() assert "innerBoundaryIs>" not in mg.to_string() - def test_1_polygons_with_holes(self): + def test_1_polygons_with_holes(self) -> None: """Test with one polygon with holes.""" p = geo.MultiPolygon( [ @@ -135,7 +135,7 @@ def test_1_polygons_with_holes(self): assert "outerBoundaryIs>" in mg.to_string() assert "innerBoundaryIs>" in mg.to_string() - def test_2_polygons(self): + def test_2_polygons(self) -> None: """Test with two polygons.""" p = geo.MultiPolygon( [ @@ -199,7 +199,7 @@ def test_2_polygons_read(self) -> None: class TestGeometryCollectionStdLibrary(StdLibrary): """Test heterogeneous geometry collections.""" - def test_1_point(self): + def test_1_point(self) -> None: """Test with one point.""" p = geo.GeometryCollection([geo.Point(1, 2)]) @@ -209,7 +209,7 @@ def test_1_point(self): assert "MultiGeometry>" in mg.to_string() assert "Point>" in mg.to_string() - def test_geometries(self): + def test_geometries(self) -> None: p = geo.Point(1, 2) ls = geo.LineString(((1, 2), (2, 0))) lr = geo.LinearRing(((0, 0), (0, 1), (1, 1), (1, 0), (0, 0))) @@ -227,7 +227,7 @@ def test_geometries(self): assert "Polygon>" in mg.to_string() assert "MultiGeometry>" in mg.to_string() - def test_multi_geometries(self): + def test_multi_geometries(self) -> None: p = geo.Point(1, 2) ls = geo.LineString(((1, 2), (2, 0))) lr = geo.LinearRing(((0, 0), (0, 1), (1, 1), (1, 0), (0, 0))) @@ -312,7 +312,13 @@ def test_multi_geometries_read(self) -> None: ), ), geo.LinearRing( - ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)), + ( + (0.0, 0.0), + (0.0, 1.0), + (1.0, 1.0), + (1.0, 0.0), + (0.0, 0.0), + ), ), ), ), diff --git a/tests/geometries/polygon_test.py b/tests/geometries/polygon_test.py index 6b791377..57dc76c4 100644 --- a/tests/geometries/polygon_test.py +++ b/tests/geometries/polygon_test.py @@ -27,7 +27,7 @@ class TestStdLibrary(StdLibrary): """Test with the standard library.""" - def test_exterior_only(self): + def test_exterior_only(self) -> None: """Test exterior only.""" poly = geo.Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]) @@ -41,7 +41,7 @@ def test_exterior_only(self): "1.000000,0.000000 0.000000,0.000000" ) in polygon.to_string() - def test_exterior_interior(self): + def test_exterior_interior(self) -> None: """Test exterior and interior.""" poly = geo.Polygon( [(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)], @@ -62,7 +62,7 @@ def test_exterior_interior(self): "0.900000,0.100000 0.100000,0.100000" ) in polygon.to_string() - def test_from_string_exterior_only(self): + def test_from_string_exterior_only(self) -> None: """Test exterior only.""" doc = """ @@ -77,7 +77,7 @@ def test_from_string_exterior_only(self): assert polygon2.geometry == geo.Polygon([(0, 0), (1, 0), (1, 1), (0, 0)]) - def test_from_string_exterior_interior(self): + def test_from_string_exterior_interior(self) -> None: doc = """ @@ -101,7 +101,7 @@ def test_from_string_exterior_interior(self): [[(0, 0), (1, 0), (1, 1), (0, 0)]], ) - def test_empty_polygon(self): + def test_empty_polygon(self) -> None: """Test empty polygon.""" doc = ( "1" diff --git a/tests/oldunit_test.py b/tests/oldunit_test.py index 6d277db8..3f71fe91 100644 --- a/tests/oldunit_test.py +++ b/tests/oldunit_test.py @@ -37,7 +37,7 @@ class TestBaseClasses: """ BaseClasses must raise a NotImplementedError on etree_element - and a TypeError on from_element + and a TypeError on from_element. """ def setup_method(self) -> None: @@ -69,7 +69,7 @@ def test_base_object(self) -> None: bo.from_element(element) assert bo.id is None assert bo.ns == config.KMLNS - assert not bo.etree_element(), None + assert bo.etree_element() is not None assert len(bo.to_string()) > 1 def test_feature(self) -> None: @@ -114,7 +114,7 @@ def test_overlay(self) -> None: class TestBuildKml: - """Build a simple KML File""" + """Build a simple KML File.""" def setup_method(self) -> None: """Always test with the same parser.""" @@ -122,7 +122,7 @@ def setup_method(self) -> None: config.set_default_namespaces() def test_kml(self) -> None: - """Kml file without contents""" + """Kml file without contents.""" k = kml.KML() assert not list(k.features()) assert ( @@ -134,7 +134,7 @@ def test_kml(self) -> None: assert k.to_string() == k2.to_string() def test_folder(self) -> None: - """KML file with folders""" + """KML file with folders.""" ns = "{http://www.opengis.net/kml/2.2}" k = kml.KML() f = kml.Folder(ns, "id", "name", "description") @@ -144,7 +144,7 @@ def test_folder(self) -> None: f2 = kml.Folder(ns, "id2", "name2", "description2") k.append(f2) assert len(list(k.features())) == 2 - assert len(list(list(k.features())[0].features())) == 1 + assert len(list(next(iter(k.features())).features())) == 1 k2 = kml.KML() s = k.to_string() k2.from_string(s) @@ -182,7 +182,7 @@ def test_document(self) -> None: f2.append(p) nf.append(p2) assert len(list(k.features())) == 1 - assert len(list(list(k.features())[0].features())) == 2 + assert len(list(next(iter(k.features())).features())) == 2 k2 = kml.KML() k2.from_string(k.to_string()) assert k.to_string() == k2.to_string() @@ -192,7 +192,9 @@ def test_author(self) -> None: d.author = "Christian Ledermann" assert "Christian Ledermann" in str(d.to_string()) a = atom.Author( - name="Nobody", uri="http://localhost", email="cl@donotreply.com", + name="Nobody", + uri="http://localhost", + email="cl@donotreply.com", ) d.author = a assert d.author == "Nobody" @@ -263,7 +265,7 @@ def test_document(self) -> None: k = kml.KML() k.from_string(doc) assert len(list(k.features())) == 1 - assert len(list(list(k.features())[0].features())) == 2 + assert len(list(next(iter(k.features())).features())) == 2 k2 = kml.KML() k2.from_string(k.to_string()) assert k.to_string() == k2.to_string() @@ -279,8 +281,8 @@ def test_document_booleans(self) -> None: k = kml.KML() k.from_string(doc) - assert list(k.features())[0].visibility == 1 - assert list(k.features())[0].isopen == 1 + assert next(iter(k.features())).visibility == 1 + assert next(iter(k.features())).isopen == 1 doc = """ Document.kml @@ -291,8 +293,8 @@ def test_document_booleans(self) -> None: k = kml.KML() k.from_string(doc) - assert list(k.features())[0].visibility == 0 - assert list(k.features())[0].isopen == 0 + assert next(iter(k.features())).visibility == 0 + assert next(iter(k.features())).isopen == 0 def test_folders(self) -> None: doc = """ @@ -338,7 +340,7 @@ def test_folders(self) -> None: k = kml.KML() k.from_string(doc) assert len(list(k.features())) == 1 - assert len(list(list(k.features())[0].features())) == 3 + assert len(list(next(iter(k.features())).features())) == 3 k2 = kml.KML() k2.from_string(k.to_string()) assert k.to_string() == k2.to_string() @@ -358,7 +360,7 @@ def test_placemark(self) -> None: k = kml.KML() k.from_string(doc) assert len(list(k.features())) == 1 - assert list(k.features())[0].name == "Simple placemark" + assert next(iter(k.features())).name == "Simple placemark" k2 = kml.KML() k2.from_string(k.to_string()) assert k.to_string() == k2.to_string() @@ -482,7 +484,7 @@ def test_polygon(self) -> None: k = kml.KML() k.from_string(doc) assert len(list(k.features())) == 1 - assert isinstance(list(k.features())[0].geometry, Polygon) + assert isinstance(next(iter(k.features())).geometry, Polygon) k2 = kml.KML() k2.from_string(k.to_string()) assert k.to_string() == k2.to_string() @@ -535,8 +537,8 @@ def test_multipoints(self) -> None: k = kml.KML() k.from_string(doc) assert len(list(k.features())) == 1 - assert isinstance(list(k.features())[0].geometry, MultiPoint) - assert len(list(list(k.features())[0].geometry.geoms)) == 12 + assert isinstance(next(iter(k.features())).geometry, MultiPoint) + assert len(list(next(iter(k.features())).geometry.geoms)) == 12 k2 = kml.KML() k2.from_string(k.to_string()) assert k.to_string() == k2.to_string() @@ -552,15 +554,15 @@ def test_snippet(self) -> None: k = kml.KML() k.from_string(doc) - assert list(k.features())[0].snippet["text"] == "Short Desc" - assert list(k.features())[0].snippet["maxLines"] == 2 - list(k.features())[0]._snippet["maxLines"] = 3 - assert list(k.features())[0].snippet["maxLines"] == 3 + assert next(iter(k.features())).snippet["text"] == "Short Desc" + assert next(iter(k.features())).snippet["maxLines"] == 2 + next(iter(k.features()))._snippet["maxLines"] = 3 + assert next(iter(k.features())).snippet["maxLines"] == 3 assert 'maxLines="3"' in k.to_string() - list(k.features())[0].snippet = {"text": "Annother Snippet"} + next(iter(k.features())).snippet = {"text": "Annother Snippet"} assert "maxLines" not in k.to_string() assert "Annother Snippet" in k.to_string() - list(k.features())[0].snippet = "Diffrent Snippet" + next(iter(k.features())).snippet = "Diffrent Snippet" assert "maxLines" not in k.to_string() assert "Diffrent Snippet" in k.to_string() @@ -665,7 +667,7 @@ def test_linarring_placemark(self) -> None: ) doc2 = kml.KML() doc2.from_string(doc.to_string()) - assert isinstance(list(doc.features())[0].geometry, LinearRing) + assert isinstance(next(iter(doc.features())).geometry, LinearRing) assert doc.to_string() == doc2.to_string() @@ -768,7 +770,7 @@ def test_styleurl(self) -> None: k = kml.KML() k.from_string(doc) assert len(list(k.features())) == 1 - assert list(k.features())[0].style_url == "#default" + assert next(iter(k.features())).style_url == "#default" k2 = kml.KML() k2.from_string(k.to_string()) assert k.to_string() == k2.to_string() @@ -803,8 +805,8 @@ def test_balloonstyle(self) -> None: k = kml.KML() k.from_string(doc) assert len(list(k.features())) == 1 - assert isinstance(list(list(k.features())[0].styles())[0], styles.Style) - style = list(list(list(k.features())[0].styles())[0].styles())[0] + assert isinstance(next(iter(next(iter(k.features())).styles())), styles.Style) + style = next(iter(next(iter(next(iter(k.features())).styles())).styles())) assert isinstance(style, styles.BalloonStyle) assert style.bg_color == "ffffffbb" assert style.text_color == "ff000000" @@ -831,8 +833,8 @@ def test_balloonstyle_old_color(self) -> None: k = kml.KML() k.from_string(doc) assert len(list(k.features())) == 1 - assert isinstance(list(list(k.features())[0].styles())[0], styles.Style) - style = list(list(list(k.features())[0].styles())[0].styles())[0] + assert isinstance(next(iter(next(iter(k.features())).styles())), styles.Style) + style = next(iter(next(iter(next(iter(k.features())).styles())).styles())) assert isinstance(style, styles.BalloonStyle) assert style.bg_color == "ffffffbb" k2 = kml.KML() @@ -855,8 +857,8 @@ def test_labelstyle(self) -> None: k = kml.KML() k.from_string(doc) assert len(list(k.features())) == 1 - assert isinstance(list(list(k.features())[0].styles())[0], styles.Style) - style = list(list(list(k.features())[0].styles())[0].styles())[0] + assert isinstance(next(iter(next(iter(k.features())).styles())), styles.Style) + style = next(iter(next(iter(next(iter(k.features())).styles())).styles())) assert isinstance(style, styles.LabelStyle) assert style.color == "ff0000cc" assert style.color_mode is None @@ -884,8 +886,8 @@ def test_iconstyle(self) -> None: k = kml.KML() k.from_string(doc) assert len(list(k.features())) == 1 - assert isinstance(list(list(k.features())[0].styles())[0], styles.Style) - style = list(list(list(k.features())[0].styles())[0].styles())[0] + assert isinstance(next(iter(next(iter(k.features())).styles())), styles.Style) + style = next(iter(next(iter(next(iter(k.features())).styles())).styles())) assert isinstance(style, styles.IconStyle) assert style.color == "ff00ff00" assert style.scale == 1.1 @@ -913,8 +915,8 @@ def test_linestyle(self) -> None: k = kml.KML() k.from_string(doc) assert len(list(k.features())) == 1 - assert isinstance(list(list(k.features())[0].styles())[0], styles.Style) - style = list(list(list(k.features())[0].styles())[0].styles())[0] + assert isinstance(next(iter(next(iter(k.features())).styles())), styles.Style) + style = next(iter(next(iter(next(iter(k.features())).styles())).styles())) assert isinstance(style, styles.LineStyle) assert style.color == "7f0000ff" assert style.width == 4 @@ -940,8 +942,8 @@ def test_polystyle(self) -> None: k = kml.KML() k.from_string(doc) assert len(list(k.features())) == 1 - assert isinstance(list(list(k.features())[0].styles())[0], styles.Style) - style = list(list(list(k.features())[0].styles())[0].styles())[0] + assert isinstance(next(iter(next(iter(k.features())).styles())), styles.Style) + style = next(iter(next(iter(next(iter(k.features())).styles())).styles())) assert isinstance(style, styles.PolyStyle) assert style.color == "ff0000cc" assert style.color_mode == "random" @@ -964,7 +966,7 @@ def test_polystyle_boolean_fill(self) -> None: k = kml.KML() k.from_string(doc) - style = list(list(list(k.features())[0].styles())[0].styles())[0] + style = next(iter(next(iter(next(iter(k.features())).styles())).styles())) assert isinstance(style, styles.PolyStyle) assert style.fill == 0 k2 = kml.KML() @@ -986,7 +988,7 @@ def test_polystyle_boolean_outline(self) -> None: k = kml.KML() k.from_string(doc) - style = list(list(list(k.features())[0].styles())[0].styles())[0] + style = next(iter(next(iter(next(iter(k.features())).styles())).styles())) assert isinstance(style, styles.PolyStyle) assert style.outline == 0 k2 = kml.KML() @@ -1008,7 +1010,7 @@ def test_polystyle_float_fill(self) -> None: k = kml.KML() k.from_string(doc) - style = list(list(list(k.features())[0].styles())[0].styles())[0] + style = next(iter(next(iter(next(iter(k.features())).styles())).styles())) assert isinstance(style, styles.PolyStyle) assert style.fill == 0 k2 = kml.KML() @@ -1030,7 +1032,7 @@ def test_polystyle_float_outline(self) -> None: k = kml.KML() k.from_string(doc) - style = list(list(list(k.features())[0].styles())[0].styles())[0] + style = next(iter(next(iter(next(iter(k.features())).styles())).styles())) assert isinstance(style, styles.PolyStyle) assert style.outline == 0 k2 = kml.KML() @@ -1069,8 +1071,8 @@ def test_styles(self) -> None: k = kml.KML() k.from_string(doc) assert len(list(k.features())) == 1 - assert isinstance(list(list(k.features())[0].styles())[0], styles.Style) - style = list(list(list(k.features())[0].styles())[0].styles()) + assert isinstance(next(iter(next(iter(k.features())).styles())), styles.Style) + style = list(next(iter(next(iter(k.features())).styles())).styles()) assert len(style) == 4 k2 = kml.KML() k2.from_string(k.to_string()) @@ -1095,8 +1097,11 @@ def test_stylemapurl(self) -> None: k = kml.KML() k.from_string(doc) assert len(list(k.features())) == 1 - assert isinstance(list(list(k.features())[0].styles())[0], styles.StyleMap) - sm = list(list(list(k.features())[0].styles()))[0] + assert isinstance( + next(iter(next(iter(k.features())).styles())), + styles.StyleMap, + ) + sm = next(iter(next(iter(k.features())).styles())) assert isinstance(sm.normal, styles.StyleUrl) assert sm.normal.url == "#normalState" assert isinstance(sm.highlight, styles.StyleUrl) @@ -1137,15 +1142,18 @@ def test_stylemapstyles(self) -> None: k = kml.KML() k.from_string(doc) assert len(list(k.features())) == 1 - assert isinstance(list(list(k.features())[0].styles())[0], styles.StyleMap) - sm = list(list(list(k.features())[0].styles()))[0] + assert isinstance( + next(iter(next(iter(k.features())).styles())), + styles.StyleMap, + ) + sm = next(iter(next(iter(k.features())).styles())) assert isinstance(sm.normal, styles.Style) assert len(list(sm.normal.styles())) == 1 - assert isinstance(list(sm.normal.styles())[0], styles.LabelStyle) + assert isinstance(next(iter(sm.normal.styles())), styles.LabelStyle) assert isinstance(sm.highlight, styles.Style) assert isinstance(sm.highlight, styles.Style) assert len(list(sm.highlight.styles())) == 2 - assert isinstance(list(sm.highlight.styles())[0], styles.LineStyle) + assert isinstance(next(iter(sm.highlight.styles())), styles.LineStyle) assert isinstance(list(sm.highlight.styles())[1], styles.PolyStyle) k2 = kml.KML() k2.from_string(k.to_string()) @@ -1183,18 +1191,18 @@ def test_get_style_by_url(self) -> None: k = kml.KML() k.from_string(doc) assert len(list(k.features())) == 1 - document = list(k.features())[0] + document = next(iter(k.features())) style = document.get_style_by_url( "http://localhost:8080/somepath#exampleStyleDocument", ) - assert isinstance(list(style.styles())[0], styles.LabelStyle) + assert isinstance(next(iter(style.styles())), styles.LabelStyle) style = document.get_style_by_url("somepath#linestyleExample") - assert isinstance(list(style.styles())[0], styles.LineStyle) + assert isinstance(next(iter(style.styles())), styles.LineStyle) style = document.get_style_by_url("#styleMapExample") assert isinstance(style, styles.StyleMap) -def test_nested_multigeometry(): +def test_nested_multigeometry() -> None: doc = """ @@ -1231,19 +1239,19 @@ def test_nested_multigeometry(): k = kml.KML() k.from_string(doc) - placemark = list(list(k.features())[0].features())[0] + placemark = next(iter(next(iter(k.features())).features())) first_multigeometry = placemark.geometry assert len(list(first_multigeometry.geoms)) == 3 - second_multigeometry = [ + second_multigeometry = next( g for g in first_multigeometry.geoms if g.geom_type == "GeometryCollection" - ][0] + ) assert len(list(second_multigeometry.geoms)) == 2 class TestGetGeometry: - def test_nested_multigeometry(self): + def test_nested_multigeometry(self) -> None: doc = """ @@ -1280,14 +1288,14 @@ def test_nested_multigeometry(self): k = kml.KML() k.from_string(doc) - placemark = list(list(k.features())[0].features())[0] + placemark = next(iter(next(iter(k.features())).features())) first_multigeometry = placemark.geometry assert len(list(first_multigeometry.geoms)) == 3 - second_multigeometry = [ + second_multigeometry = next( g for g in first_multigeometry.geoms if g.geom_type == "GeometryCollection" - ][0] + ) assert len(list(second_multigeometry.geoms)) == 2 @@ -1743,7 +1751,12 @@ def test_camera_altitude_mode_absolute(self) -> None: def test_camera_initialization(self) -> None: self.p.camera = kml.Camera( - longitude=10, latitude=20, altitude=30, heading=40, tilt=50, roll=60, + longitude=10, + latitude=20, + altitude=30, + heading=40, + tilt=50, + roll=60, ) assert self.p.camera.longitude == 10 assert self.p.camera.latitude == 20 diff --git a/tests/styles_test.py b/tests/styles_test.py index 35d8c61e..f904e9b6 100644 --- a/tests/styles_test.py +++ b/tests/styles_test.py @@ -355,12 +355,12 @@ def test_style_read(self) -> None: assert style.id == "id-0" assert style.target_id == "target-0" - assert list(style.styles())[0].id == "id-b0" - assert list(style.styles())[0].target_id == "target-b0" - assert list(style.styles())[0].bg_color == "7fff0000" # type: ignore[union-attr] - assert list(style.styles())[0].text_color == "ff00ff00" # type: ignore[union-attr] - assert list(style.styles())[0].text == "Hello" # type: ignore[union-attr] - assert list(style.styles())[0].display_mode == "hide" # type: ignore[union-attr] + assert next(iter(style.styles())).id == "id-b0" + assert next(iter(style.styles())).target_id == "target-b0" + assert next(iter(style.styles())).bg_color == "7fff0000" # type: ignore[union-attr] + assert next(iter(style.styles())).text_color == "ff00ff00" # type: ignore[union-attr] + assert next(iter(style.styles())).text == "Hello" # type: ignore[union-attr] + assert next(iter(style.styles())).display_mode == "hide" # type: ignore[union-attr] assert list(style.styles())[1].id == "id-i0" assert list(style.styles())[1].target_id == "target-i0" From 016f5cdcb5667fade23c1ee4a61ea3823fa4e153 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 11 Nov 2023 16:40:57 +0000 Subject: [PATCH 38/63] Refactor XML object initialization to include name_spaces parameter --- fastkml/base.py | 28 +++++++++++++++++++++------- fastkml/data.py | 12 ++++++++---- fastkml/geometry.py | 13 ++++++++++++- fastkml/gx.py | 4 ++++ fastkml/styles.py | 24 +++++++++++++++++++----- tests/data_test.py | 4 ++-- 6 files changed, 66 insertions(+), 19 deletions(-) diff --git a/fastkml/base.py b/fastkml/base.py index e2467e20..64e78887 100644 --- a/fastkml/base.py +++ b/fastkml/base.py @@ -138,11 +138,12 @@ def _get_kwargs( cls, *, ns: str, + name_spaces: Optional[Dict[str, str]] = None, element: Element, strict: bool, ) -> Dict[str, Any]: """Returns a dictionary of kwargs for the class constructor.""" - kwargs: Dict[str, Any] = {} + kwargs: Dict[str, Any] = {"ns": ns, "name_spaces": name_spaces} return kwargs @classmethod @@ -150,13 +151,13 @@ def class_from_element( cls, *, ns: str, + name_spaces: Optional[Dict[str, str]] = None, element: Element, strict: bool, ) -> "_XMLObject": """Creates an XML object from an etree element.""" kwargs = cls._get_kwargs(ns=ns, element=element, strict=strict) return cls( - ns=ns, **kwargs, ) @@ -166,6 +167,7 @@ def class_from_string( string: str, *, ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, strict: bool = True, ) -> "_XMLObject": """ @@ -228,11 +230,12 @@ class _BaseObject(_XMLObject): def __init__( self, ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, id: Optional[str] = None, target_id: Optional[str] = None, ) -> None: """Initialize the KML Object.""" - super().__init__(ns) + super().__init__(ns=ns, name_spaces=name_spaces) self.id = id self.target_id = target_id @@ -247,6 +250,7 @@ def __eq__(self, other: object) -> bool: def __repr__(self) -> str: return ( f"{self.__class__.__name__}(ns={self.ns!r}, " + f"name_spaces={self.name_spaces!r}, " f"(id={self.id!r}, target_id={self.target_id!r})" ) @@ -275,11 +279,21 @@ def _get_kwargs( cls, *, ns: str, + name_spaces: Optional[Dict[str, str]] = None, element: Element, strict: bool, ) -> Dict[str, Any]: """Get the keyword arguments to build the object from an element.""" - return { - "id": cls._get_id(element=element, strict=strict), - "target_id": cls._get_target_id(element=element, strict=strict), - } + kwargs = super()._get_kwargs( + ns=ns, + name_spaces=name_spaces, + element=element, + strict=strict, + ) + kwargs.update( + { + "id": cls._get_id(element=element, strict=strict), + "target_id": cls._get_target_id(element=element, strict=strict), + }, + ) + return kwargs diff --git a/fastkml/data.py b/fastkml/data.py index 491649c6..99efbf2f 100644 --- a/fastkml/data.py +++ b/fastkml/data.py @@ -92,6 +92,7 @@ class Schema(_BaseObject): def __init__( self, ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, id: Optional[str] = None, target_id: Optional[str] = None, name: Optional[str] = None, @@ -100,7 +101,7 @@ def __init__( if id is None: msg = "Id is required for schema" raise KMLSchemaError(msg) - super().__init__(ns=ns, id=id, target_id=target_id) + super().__init__(ns=ns, name_spaces=name_spaces, id=id, target_id=target_id) self.name = name self._simple_fields = list(fields) if fields else [] @@ -198,11 +199,12 @@ class Data(_XMLObject): def __init__( self, ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, name: Optional[str] = None, value: Optional[str] = None, display_name: Optional[str] = None, ) -> None: - super().__init__(ns) + super().__init__(ns=ns, name_spaces=name_spaces) self.name = name self.value = value @@ -279,10 +281,11 @@ class SchemaData(_XMLObject): def __init__( self, ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, schema_url: Optional[str] = None, data: Optional[Iterable[SimpleData]] = None, ) -> None: - super().__init__(ns) + super().__init__(ns=ns, name_spaces=name_spaces) if (not isinstance(schema_url, str)) or (not schema_url): msg = "required parameter schema_url missing" raise ValueError(msg) @@ -356,9 +359,10 @@ class ExtendedData(_XMLObject): def __init__( self, ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, elements: Optional[Iterable[Union[Data, SchemaData]]] = None, ) -> None: - super().__init__(ns) + super().__init__(ns=ns, name_spaces=name_spaces) self.elements = elements or [] def __repr__(self) -> str: diff --git a/fastkml/geometry.py b/fastkml/geometry.py index 84176b04..d3456552 100644 --- a/fastkml/geometry.py +++ b/fastkml/geometry.py @@ -78,6 +78,7 @@ def __init__( self, *, ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, id: Optional[str] = None, target_id: Optional[str] = None, extrude: Optional[bool] = False, @@ -97,7 +98,7 @@ def __init__( altitude_mode: Specifies how altitude components in the element are interpreted. """ - super().__init__(ns=ns, id=id, target_id=target_id) + super().__init__(ns=ns, id=id, name_spaces=name_spaces, target_id=target_id) self._extrude = extrude self._tessellate = tessellate self._altitude_mode = altitude_mode @@ -327,6 +328,7 @@ def __init__( self, *, ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, id: Optional[str] = None, target_id: Optional[str] = None, extrude: Optional[bool] = False, @@ -337,6 +339,7 @@ def __init__( super().__init__( ns=ns, id=id, + name_spaces=name_spaces, target_id=target_id, extrude=extrude, tessellate=tessellate, @@ -381,6 +384,7 @@ def __init__( self, *, ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, id: Optional[str] = None, target_id: Optional[str] = None, extrude: Optional[bool] = False, @@ -390,6 +394,7 @@ def __init__( ) -> None: super().__init__( ns=ns, + name_spaces=name_spaces, id=id, target_id=target_id, extrude=extrude, @@ -435,6 +440,7 @@ def __init__( self, *, ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, id: Optional[str] = None, target_id: Optional[str] = None, extrude: Optional[bool] = False, @@ -444,6 +450,7 @@ def __init__( ) -> None: super().__init__( ns=ns, + name_spaces=name_spaces, id=id, target_id=target_id, extrude=extrude, @@ -477,6 +484,7 @@ def __init__( self, *, ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, id: Optional[str] = None, target_id: Optional[str] = None, extrude: Optional[bool] = False, @@ -486,6 +494,7 @@ def __init__( ) -> None: super().__init__( ns=ns, + name_spaces=name_spaces, id=id, target_id=target_id, extrude=extrude, @@ -619,6 +628,7 @@ def __init__( self, *, ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, id: Optional[str] = None, target_id: Optional[str] = None, extrude: Optional[bool] = False, @@ -628,6 +638,7 @@ def __init__( ) -> None: super().__init__( ns=ns, + name_spaces=name_spaces, id=id, target_id=target_id, extrude=extrude, diff --git a/fastkml/gx.py b/fastkml/gx.py index 9dbb8780..c9cc4b02 100644 --- a/fastkml/gx.py +++ b/fastkml/gx.py @@ -193,6 +193,7 @@ def __init__( self, *, ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, id: Optional[str] = None, target_id: Optional[str] = None, extrude: Optional[bool] = False, @@ -211,6 +212,7 @@ def __init__( self.track_items = track_items super().__init__( ns=ns, + name_spaces=name_spaces, id=id, target_id=target_id, extrude=extrude, @@ -318,6 +320,7 @@ def __init__( self, *, ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, id: Optional[str] = None, target_id: Optional[str] = None, extrude: Optional[bool] = False, @@ -338,6 +341,7 @@ def __init__( self.interpolate = interpolate super().__init__( ns=ns, + name_spaces=name_spaces, id=id, target_id=target_id, extrude=extrude, diff --git a/fastkml/styles.py b/fastkml/styles.py index 1bc551fb..c18c739e 100644 --- a/fastkml/styles.py +++ b/fastkml/styles.py @@ -21,6 +21,7 @@ """ import logging +from typing import Dict from typing import Iterable from typing import Iterator from typing import List @@ -51,11 +52,12 @@ class StyleUrl(_BaseObject): def __init__( self, ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, id: Optional[str] = None, target_id: Optional[str] = None, url: Optional[str] = None, ) -> None: - super().__init__(ns=ns, id=id, target_id=target_id) + super().__init__(ns=ns, name_spaces=name_spaces, id=id, target_id=target_id) self.url = url def etree_element( @@ -109,12 +111,13 @@ class _ColorStyle(_BaseObject): def __init__( self, ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, id: Optional[str] = None, target_id: Optional[str] = None, color: Optional[str] = None, color_mode: Optional[str] = None, ) -> None: - super().__init__(ns=ns, id=id, target_id=target_id) + super().__init__(ns=ns, name_spaces=name_spaces, id=id, target_id=target_id) self.color = color self.color_mode = color_mode @@ -171,6 +174,7 @@ class IconStyle(_ColorStyle): def __init__( self, ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, id: Optional[str] = None, target_id: Optional[str] = None, color: Optional[str] = None, @@ -182,6 +186,7 @@ def __init__( ) -> None: super().__init__( ns=ns, + name_spaces=name_spaces, id=id, target_id=target_id, color=color, @@ -270,6 +275,7 @@ class LineStyle(_ColorStyle): def __init__( self, ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, id: Optional[str] = None, target_id: Optional[str] = None, color: Optional[str] = None, @@ -278,6 +284,7 @@ def __init__( ) -> None: super().__init__( ns=ns, + name_spaces=name_spaces, id=id, target_id=target_id, color=color, @@ -323,6 +330,7 @@ class PolyStyle(_ColorStyle): def __init__( self, ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, id: Optional[str] = None, target_id: Optional[str] = None, color: Optional[str] = None, @@ -332,6 +340,7 @@ def __init__( ) -> None: super().__init__( ns=ns, + name_spaces=name_spaces, id=id, target_id=target_id, color=color, @@ -388,6 +397,7 @@ class LabelStyle(_ColorStyle): def __init__( self, ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, id: Optional[str] = None, target_id: Optional[str] = None, color: Optional[str] = None, @@ -396,6 +406,7 @@ def __init__( ) -> None: super().__init__( ns=ns, + name_spaces=name_spaces, id=id, target_id=target_id, color=color, @@ -480,6 +491,7 @@ class BalloonStyle(_BaseObject): def __init__( self, ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, id: Optional[str] = None, target_id: Optional[str] = None, bg_color: Optional[str] = None, @@ -487,7 +499,7 @@ def __init__( text: Optional[str] = None, display_mode: Optional[str] = None, ) -> None: - super().__init__(ns=ns, id=id, target_id=target_id) + super().__init__(ns=ns, name_spaces=name_spaces, id=id, target_id=target_id) self.bg_color = bg_color self.text_color = text_color self.text = text @@ -563,11 +575,12 @@ class Style(_StyleSelector): def __init__( self, ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, id: Optional[str] = None, target_id: Optional[str] = None, styles: Optional[Iterable[AnyStyle]] = None, ) -> None: - super().__init__(ns, id, target_id) + super().__init__(ns=ns, name_spaces=name_spaces, id=id, target_id=target_id) self._styles: List[AnyStyle] = [] if styles: for style in styles: @@ -629,12 +642,13 @@ class StyleMap(_StyleSelector): def __init__( self, ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, id: Optional[str] = None, target_id: Optional[str] = None, normal: Optional[Union[Style, StyleUrl]] = None, highlight: Optional[Union[Style, StyleUrl]] = None, ) -> None: - super().__init__(ns, id, target_id) + super().__init__(ns=ns, name_spaces=name_spaces, id=id, target_id=target_id) self.normal = normal self.highlight = highlight diff --git a/tests/data_test.py b/tests/data_test.py index 652b1a69..392bf327 100644 --- a/tests/data_test.py +++ b/tests/data_test.py @@ -33,7 +33,7 @@ def test_schema_requires_id(self) -> None: def test_schema(self) -> None: ns = "{http://www.opengis.net/kml/2.2}" - s = kml.Schema(ns, "some_id") + s = kml.Schema(ns=ns, id="some_id") assert not list(s.simple_fields) field = data.SimpleField( name="Integer", @@ -99,7 +99,7 @@ def test_schema_data(self) -> None: ns = "{http://www.opengis.net/kml/2.2}" pytest.raises(ValueError, data.SchemaData, ns) pytest.raises(ValueError, data.SchemaData, ns, "") - sd = data.SchemaData(ns, "#default") + sd = data.SchemaData(ns=ns, schema_url="#default") sd.append_data(data.SimpleData("text", "Some Text")) assert len(sd.data) == 1 sd.append_data(data.SimpleData(value=1, name="Integer")) From 966acc26830fcee26e768ad37e8c8632c525e368 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 11 Nov 2023 17:09:02 +0000 Subject: [PATCH 39/63] Add AltitudeMode enum to camera and look_at tests #194 --- fastkml/views.py | 44 +++++++++++++++++++++++++------------------ tests/oldunit_test.py | 13 +++++-------- tests/views_test.py | 23 ++++++++++++++-------- 3 files changed, 46 insertions(+), 34 deletions(-) diff --git a/fastkml/views.py b/fastkml/views.py index 17b738a9..466145c5 100644 --- a/fastkml/views.py +++ b/fastkml/views.py @@ -1,4 +1,5 @@ import logging +from typing import Dict from typing import Optional from typing import SupportsFloat from typing import Union @@ -6,6 +7,7 @@ from fastkml import config from fastkml import gx from fastkml.base import _BaseObject +from fastkml.enums import AltitudeMode from fastkml.enums import Verbosity from fastkml.mixins import TimeMixin from fastkml.times import TimeSpan @@ -46,7 +48,7 @@ class _AbstractView(TimeMixin, _BaseObject): # view is pointed up into the sky. Values for are clamped at +180 # degrees. - _altitude_mode: str = "relativeToGround" + _altitude_mode: AltitudeMode # Specifies how the specified for the Camera is interpreted. # Possible values are as follows: # relativeToGround - @@ -64,6 +66,7 @@ class _AbstractView(TimeMixin, _BaseObject): def __init__( self, ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, id: Optional[str] = None, target_id: Optional[str] = None, longitude: Optional[float] = None, @@ -71,10 +74,10 @@ def __init__( altitude: Optional[float] = None, heading: Optional[float] = None, tilt: Optional[float] = None, - altitude_mode: str = "relativeToGround", + altitude_mode: AltitudeMode = AltitudeMode.relative_to_ground, time_primitive: Union[TimeSpan, TimeStamp, None] = None, ) -> None: - super().__init__(ns=ns, id=id, target_id=target_id) + super().__init__(ns=ns, name_spaces=name_spaces, id=id, target_id=target_id) self._longitude = longitude self._latitude = latitude self._altitude = altitude @@ -152,18 +155,12 @@ def tilt(self, value: Optional[SupportsFloat]) -> None: raise ValueError @property - def altitude_mode(self) -> str: + def altitude_mode(self) -> AltitudeMode: return self._altitude_mode @altitude_mode.setter - def altitude_mode(self, mode: str) -> None: - if mode in {"relativeToGround", "clampToGround", "absolute"}: - self._altitude_mode = mode - else: - self._altitude_mode = "relativeToGround" - # raise ValueError( - # "altitude_mode must be one of " "relativeToGround, - # clampToGround, absolute") + def altitude_mode(self, mode: AltitudeMode) -> None: + self._altitude_mode = mode def from_element(self, element: Element): super().from_element(element) @@ -185,7 +182,7 @@ def from_element(self, element: Element): altitude_mode = element.find(f"{self.ns}altitudeMode") if altitude_mode is None: altitude_mode = element.find(f"{gx.NS}altitudeMode") - self.altitude_mode = altitude_mode.text + self.altitude_mode = AltitudeMode(altitude_mode.text) timespan = element.find(f"{self.ns}TimeSpan") if timespan is not None: s = TimeSpan(self.ns) @@ -218,11 +215,18 @@ def etree_element( if self.tilt: tilt = config.etree.SubElement(element, f"{self.ns}tilt") tilt.text = str(self.tilt) - if self.altitude_mode in ("clampedToGround", "relativeToGround", "absolute"): + if self.altitude_mode in ( + AltitudeMode.clamp_to_ground, + AltitudeMode.relative_to_ground, + AltitudeMode.absolute, + ): altitude_mode = config.etree.SubElement(element, f"{self.ns}altitudeMode") - elif self.altitude_mode in ("clampedToSeaFloor", "relativeToSeaFloor"): + elif self.altitude_mode in ( + AltitudeMode.clamp_to_sea_floor, + AltitudeMode.relative_to_sea_floor, + ): altitude_mode = config.etree.SubElement(element, f"{gx.NS}altitudeMode") - altitude_mode.text = self.altitude_mode + altitude_mode.text = self.altitude_mode.value if (self._timespan is not None) and (self._timestamp is not None): msg = "Either Timestamp or Timespan can be defined, not both" raise ValueError(msg) @@ -267,6 +271,7 @@ class Camera(_AbstractView): def __init__( self, ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, id: Optional[str] = None, target_id: Optional[str] = None, longitude: Optional[float] = None, @@ -275,11 +280,12 @@ def __init__( heading: Optional[float] = None, tilt: Optional[float] = None, roll: Optional[float] = None, - altitude_mode: str = "relativeToGround", + altitude_mode: AltitudeMode = AltitudeMode.relative_to_ground, time_primitive: Union[TimeSpan, TimeStamp, None] = None, ) -> None: super().__init__( ns=ns, + name_spaces=name_spaces, id=id, target_id=target_id, longitude=longitude, @@ -333,6 +339,7 @@ class LookAt(_AbstractView): def __init__( self, ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, id: Optional[str] = None, target_id: Optional[str] = None, longitude: Optional[float] = None, @@ -341,11 +348,12 @@ def __init__( heading: Optional[float] = None, tilt: Optional[float] = None, range: Optional[float] = None, - altitude_mode: str = "relativeToGround", + altitude_mode: AltitudeMode = AltitudeMode.relative_to_ground, time_primitive: Union[TimeSpan, TimeStamp, None] = None, ) -> None: super().__init__( ns=ns, + name_spaces=name_spaces, id=id, target_id=target_id, longitude=longitude, diff --git a/tests/oldunit_test.py b/tests/oldunit_test.py index 3f71fe91..01774e2b 100644 --- a/tests/oldunit_test.py +++ b/tests/oldunit_test.py @@ -25,6 +25,7 @@ from fastkml import config from fastkml import kml from fastkml import styles +from fastkml.enums import AltitudeMode try: import lxml @@ -1735,15 +1736,11 @@ def test_camera_altitude_none(self) -> None: assert self.p.camera.altitude is None def test_camera_altitude_mode_default(self) -> None: - assert self.p.camera.altitude_mode == "relativeToGround" - - def test_camera_altitude_mode_error(self) -> None: - self.p.camera.altitude_mode = "" - assert self.p.camera.altitude_mode == "relativeToGround" + assert self.p.camera.altitude_mode == AltitudeMode("relativeToGround") def test_camera_altitude_mode_clamp(self) -> None: - self.p.camera.altitude_mode = "clampToGround" - assert self.p.camera.altitude_mode == "clampToGround" + self.p.camera.altitude_mode = AltitudeMode("clampToGround") + assert self.p.camera.altitude_mode == AltitudeMode("clampToGround") def test_camera_altitude_mode_absolute(self) -> None: self.p.camera.altitude_mode = "absolute" @@ -1764,4 +1761,4 @@ def test_camera_initialization(self) -> None: assert self.p.camera.heading == 40 assert self.p.camera.tilt == 50 assert self.p.camera.roll == 60 - assert self.p.camera.altitude_mode == "relativeToGround" + assert self.p.camera.altitude_mode == AltitudeMode("relativeToGround") diff --git a/tests/views_test.py b/tests/views_test.py index dd453fd1..ae51cb54 100644 --- a/tests/views_test.py +++ b/tests/views_test.py @@ -22,6 +22,7 @@ from fastkml import times from fastkml import views +from fastkml.enums import AltitudeMode from tests.base import Lxml from tests.base import StdLibrary @@ -44,7 +45,7 @@ def test_create_camera(self) -> None: tilt=20, roll=30, altitude=40, - altitude_mode="relativeToGround", + altitude_mode=AltitudeMode("relativeToGround"), latitude=50, longitude=60, time_primitive=time_span, @@ -54,7 +55,7 @@ def test_create_camera(self) -> None: assert camera.tilt == 20 assert camera.roll == 30 assert camera.altitude == 40 - assert camera.altitude_mode == "relativeToGround" + assert camera.altitude_mode == AltitudeMode("relativeToGround") assert camera.latitude == 50 assert camera.longitude == 60 assert camera.id == "cam-id" @@ -93,7 +94,7 @@ def test_camera_read(self) -> None: assert camera.tilt == 20 assert camera.roll == 30 assert camera.altitude == 40 - assert camera.altitude_mode == "relativeToGround" + assert camera.altitude_mode == AltitudeMode("relativeToGround") assert camera.latitude == 50 assert camera.longitude == 60 assert camera.id == "cam-id" @@ -117,7 +118,7 @@ def test_create_look_at(self) -> None: heading=10, tilt=20, range=30, - altitude_mode="relativeToGround", + altitude_mode=AltitudeMode("relativeToGround"), latitude=50, longitude=60, time_primitive=time_stamp, @@ -126,13 +127,16 @@ def test_create_look_at(self) -> None: assert look_at.heading == 10 assert look_at.tilt == 20 assert look_at.range == 30 - assert look_at.altitude_mode == "relativeToGround" + assert look_at.altitude_mode == AltitudeMode("relativeToGround") assert look_at.latitude == 50 assert look_at.longitude == 60 assert look_at.id == "look-at-id" assert look_at.target_id == "target-look-at-id" assert look_at._timestamp.timestamp.dt == datetime.datetime( - 2019, 1, 1, tzinfo=tzutc(), + 2019, + 1, + 1, + tzinfo=tzutc(), ) assert look_at.begin is None assert look_at.end is None @@ -160,13 +164,16 @@ def test_look_at_read(self) -> None: assert look_at.heading == 10 assert look_at.tilt == 20 assert look_at.range == 30 - assert look_at.altitude_mode == "relativeToGround" + assert look_at.altitude_mode == AltitudeMode("relativeToGround") assert look_at.latitude == 50 assert look_at.longitude == 60 assert look_at.id == "look-at-id" assert look_at.target_id == "target-look-at-id" assert look_at._timestamp.timestamp.dt == datetime.datetime( - 2019, 1, 1, tzinfo=tzutc(), + 2019, + 1, + 1, + tzinfo=tzutc(), ) assert look_at.begin is None assert look_at.end is None From 56e3938fd03c8a2de46cb90a5c39ff1ed2809a80 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 11 Nov 2023 17:54:17 +0000 Subject: [PATCH 40/63] Refactor XML parsing in fastkml package #216 #187 --- .pre-commit-config.yaml | 4 -- fastkml/data.py | 20 ++++++++-- fastkml/geometry.py | 5 ++- fastkml/gx.py | 10 ++++- fastkml/views.py | 88 ++++++++++++++++++++++------------------- pyproject.toml | 1 - 6 files changed, 76 insertions(+), 52 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 346b476e..30fa6d15 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -50,10 +50,6 @@ repos: rev: 23.10.1 hooks: - id: black - - repo: https://github.com/PyCQA/flake8 - rev: 6.1.0 - hooks: - - id: flake8 - repo: https://github.com/pycqa/isort rev: 5.12.0 hooks: diff --git a/fastkml/data.py b/fastkml/data.py index 99efbf2f..05fd60b0 100644 --- a/fastkml/data.py +++ b/fastkml/data.py @@ -178,10 +178,13 @@ def _get_kwargs( cls, *, ns: str, + name_spaces: Optional[Dict[str, str]] = None, element: Element, strict: bool, ) -> Dict[str, Any]: - kwargs = super()._get_kwargs(ns=ns, element=element, strict=strict) + kwargs = super()._get_kwargs( + ns=ns, name_spaces=name_spaces, element=element, strict=strict + ) kwargs["name"] = element.get("name") kwargs["fields"] = cls._get_fields_kwargs_from_element( ns=ns, @@ -243,10 +246,13 @@ def _get_kwargs( cls, *, ns: str, + name_spaces: Optional[Dict[str, str]] = None, element: Element, strict: bool, ) -> Dict[str, Any]: - kwargs = super()._get_kwargs(ns=ns, element=element, strict=strict) + kwargs = super()._get_kwargs( + ns=ns, name_spaces=name_spaces, element=element, strict=strict + ) kwargs["name"] = element.get("name") value = element.find(f"{ns}value") if value is not None: @@ -332,10 +338,13 @@ def _get_kwargs( cls, *, ns: str, + name_spaces: Optional[Dict[str, str]] = None, element: Element, strict: bool, ) -> Dict[str, Any]: - kwargs = super()._get_kwargs(ns=ns, element=element, strict=strict) + kwargs = super()._get_kwargs( + ns=ns, name_spaces=name_spaces, element=element, strict=strict + ) kwargs["schema_url"] = element.get("schemaUrl") kwargs["data"] = [ SimpleData(name=sd.get("name"), value=sd.text) @@ -387,10 +396,13 @@ def _get_kwargs( cls, *, ns: str, + name_spaces: Optional[Dict[str, str]] = None, element: Element, strict: bool, ) -> Dict[str, Any]: - kwargs = super()._get_kwargs(ns=ns, element=element, strict=strict) + kwargs = super()._get_kwargs( + ns=ns, name_spaces=name_spaces, element=element, strict=strict + ) elements = [] untyped_data = element.findall(f"{ns}Data") for ud in untyped_data: diff --git a/fastkml/geometry.py b/fastkml/geometry.py index d3456552..e9d84f32 100644 --- a/fastkml/geometry.py +++ b/fastkml/geometry.py @@ -312,10 +312,13 @@ def _get_kwargs( cls, *, ns: str, + name_spaces: Optional[Dict[str, str]] = None, element: Element, strict: bool, ) -> Dict[str, Any]: - kwargs = super()._get_kwargs(ns=ns, element=element, strict=strict) + kwargs = super()._get_kwargs( + ns=ns, name_spaces=name_spaces, element=element, strict=strict + ) kwargs.update(cls._get_geometry_kwargs(ns=ns, element=element, strict=strict)) kwargs.update( {"geometry": cls._get_geometry(ns=ns, element=element, strict=strict)}, diff --git a/fastkml/gx.py b/fastkml/gx.py index c9cc4b02..5a81c8b7 100644 --- a/fastkml/gx.py +++ b/fastkml/gx.py @@ -290,10 +290,13 @@ def _get_kwargs( cls, *, ns: str, + name_spaces: Optional[Dict[str, str]] = None, element: Element, strict: bool, ) -> Dict[str, Any]: - kwargs = super()._get_kwargs(ns=ns, element=element, strict=strict) + kwargs = super()._get_kwargs( + ns=ns, name_spaces=name_spaces, element=element, strict=strict + ) kwargs["track_items"] = cls.track_items_kwargs_from_element( ns=ns, element=element, @@ -432,10 +435,13 @@ def _get_kwargs( cls, *, ns: str, + name_spaces: Optional[Dict[str, str]] = None, element: Element, strict: bool, ) -> Dict[str, Any]: - kwargs = super()._get_kwargs(ns=ns, element=element, strict=strict) + kwargs = super()._get_kwargs( + ns=ns, name_spaces=name_spaces, element=element, strict=strict + ) kwargs["interpolate"] = cls._get_interpolate( ns=ns, element=element, diff --git a/fastkml/views.py b/fastkml/views.py index 466145c5..15659aea 100644 --- a/fastkml/views.py +++ b/fastkml/views.py @@ -5,7 +5,6 @@ from typing import Union from fastkml import config -from fastkml import gx from fastkml.base import _BaseObject from fastkml.enums import AltitudeMode from fastkml.enums import Verbosity @@ -162,37 +161,38 @@ def altitude_mode(self) -> AltitudeMode: def altitude_mode(self, mode: AltitudeMode) -> None: self._altitude_mode = mode - def from_element(self, element: Element): + def from_element(self, element: Element) -> None: super().from_element(element) longitude = element.find(f"{self.ns}longitude") if longitude is not None: - self.longitude = longitude.text + self.longitude = float(longitude.text) latitude = element.find(f"{self.ns}latitude") if latitude is not None: - self.latitude = latitude.text + self.latitude = float(latitude.text) altitude = element.find(f"{self.ns}altitude") if altitude is not None: - self.altitude = altitude.text + self.altitude = float(altitude.text) heading = element.find(f"{self.ns}heading") if heading is not None: - self.heading = heading.text + self.heading = float(heading.text) tilt = element.find(f"{self.ns}tilt") if tilt is not None: - self.tilt = tilt.text + self.tilt = float(tilt.text) altitude_mode = element.find(f"{self.ns}altitudeMode") if altitude_mode is None: - altitude_mode = element.find(f"{gx.NS}altitudeMode") - self.altitude_mode = AltitudeMode(altitude_mode.text) + altitude_mode = element.find(f"{self.name_spaces['gx']}altitudeMode") + if altitude_mode is not None: + self.altitude_mode = AltitudeMode(altitude_mode.text) timespan = element.find(f"{self.ns}TimeSpan") if timespan is not None: - s = TimeSpan(self.ns) - s.from_element(timespan) - self._timespan = s + span = TimeSpan(self.ns) + span.from_element(timespan) + self._timespan = span timestamp = element.find(f"{self.ns}TimeStamp") if timestamp is not None: - s = TimeStamp(self.ns) - s.from_element(timestamp) - self._timestamp = s + stamp = TimeStamp(self.ns) + stamp.from_element(timestamp) + self._timestamp = stamp def etree_element( self, @@ -201,31 +201,45 @@ def etree_element( ) -> Element: element = super().etree_element(precision=precision, verbosity=verbosity) if self.longitude: - longitude = config.etree.SubElement(element, f"{self.ns}longitude") + longitude = config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.ns}longitude" + ) longitude.text = str(self.longitude) if self.latitude: - latitude = config.etree.SubElement(element, f"{self.ns}latitude") + latitude = config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.ns}latitude" + ) latitude.text = str(self.latitude) if self.altitude: - altitude = config.etree.SubElement(element, f"{self.ns}altitude") + altitude = config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.ns}altitude" + ) altitude.text = str(self.altitude) if self.heading: - heading = config.etree.SubElement(element, f"{self.ns}heading") + heading = config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.ns}heading" + ) heading.text = str(self.heading) if self.tilt: - tilt = config.etree.SubElement(element, f"{self.ns}tilt") + tilt = config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.ns}tilt" + ) tilt.text = str(self.tilt) if self.altitude_mode in ( AltitudeMode.clamp_to_ground, AltitudeMode.relative_to_ground, AltitudeMode.absolute, ): - altitude_mode = config.etree.SubElement(element, f"{self.ns}altitudeMode") + altitude_mode = config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.ns}altitudeMode" + ) elif self.altitude_mode in ( AltitudeMode.clamp_to_sea_floor, AltitudeMode.relative_to_sea_floor, ): - altitude_mode = config.etree.SubElement(element, f"{gx.NS}altitudeMode") + altitude_mode = config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.name_spaces['gx']}altitudeMode" + ) altitude_mode.text = self.altitude_mode.value if (self._timespan is not None) and (self._timestamp is not None): msg = "Either Timestamp or Timespan can be defined, not both" @@ -302,7 +316,7 @@ def from_element(self, element: Element) -> None: super().from_element(element) roll = element.find(f"{self.ns}roll") if roll is not None: - self.roll = roll.text + self.roll = float(roll.text) def etree_element( self, @@ -311,7 +325,9 @@ def etree_element( ) -> Element: element = super().etree_element(precision=precision, verbosity=verbosity) if self.roll: - roll = config.etree.SubElement(element, f"{self.ns}roll") + roll = config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.ns}roll" + ) roll.text = str(self.roll) return element @@ -320,13 +336,8 @@ def roll(self) -> Optional[float]: return self._roll @roll.setter - def roll(self, value) -> None: - if isinstance(value, (str, int, float)) and (-180 <= float(value) <= 180): - self._roll = float(value) - elif value is None: - self._roll = None - else: - raise ValueError + def roll(self, value: float) -> None: + self._roll = value class LookAt(_AbstractView): @@ -371,19 +382,14 @@ def range(self) -> Optional[float]: return self._range @range.setter - def range(self, value) -> None: - if isinstance(value, (str, int, float)): - self._range = float(value) - elif value is None: - self._range = None - else: - raise ValueError + def range(self, value: float) -> None: + self._range = value def from_element(self, element: Element) -> None: super().from_element(element) range_var = element.find(f"{self.ns}range") if range_var is not None: - self.range = range_var.text + self.range = float(range_var.text) def etree_element( self, @@ -392,7 +398,9 @@ def etree_element( ) -> Element: element = super().etree_element(precision=precision, verbosity=verbosity) if self.range: - range_var = config.etree.SubElement(element, f"{self.ns}range") + range_var = config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.ns}range" + ) range_var.text = str(self._range) return element diff --git a/pyproject.toml b/pyproject.toml index 8e0f20b9..5a0dbb62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -141,7 +141,6 @@ warn_unused_ignores = true ignore_errors = true module = [ "fastkml.kml", - "fastkml.views", ] [tool.pyright] From 8df1a75eb8e4826eff51b47a27eaf24116e62732 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 11 Nov 2023 18:11:00 +0000 Subject: [PATCH 41/63] Add type hinting for Dict in snippet property and use Optional instead of Union in several properties --- fastkml/kml.py | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/fastkml/kml.py b/fastkml/kml.py index 8978e29c..82564b65 100644 --- a/fastkml/kml.py +++ b/fastkml/kml.py @@ -28,6 +28,7 @@ import urllib.parse as urlparse from datetime import datetime from typing import Any +from typing import Dict from typing import Iterator from typing import List from typing import Optional @@ -291,7 +292,7 @@ def styles(self) -> Iterator[Union[Style, StyleMap]]: raise TypeError @property - def snippet(self) -> dict | None | dict[str, Any]: + def snippet(self) -> Optional[Dict[str, Any]]: if not self._snippet: return None if isinstance(self._snippet, dict): @@ -946,7 +947,7 @@ def color(self, color) -> None: raise ValueError @property - def draw_order(self) -> str | None: + def draw_order(self) -> Optional[str]: return self._draw_order @draw_order.setter @@ -959,7 +960,7 @@ def draw_order(self, value) -> None: raise ValueError @property - def icon(self) -> Icon | None: + def icon(self) -> Optional[Icon]: return self._icon @icon.setter @@ -1093,7 +1094,7 @@ class PhotoOverlay(_Overlay): # for spherical panoramas @property - def rotation(self) -> str | None: + def rotation(self) -> Optional[str]: return self._rotation @rotation.setter @@ -1106,7 +1107,7 @@ def rotation(self, value) -> None: raise ValueError @property - def left_fov(self) -> str | None: + def left_fov(self) -> Optional[str]: return self._left_fow @left_fov.setter @@ -1119,7 +1120,7 @@ def left_fov(self, value) -> None: raise ValueError @property - def right_fov(self) -> str | None: + def right_fov(self) -> Optional[str]: return self._right_fov @right_fov.setter @@ -1132,7 +1133,7 @@ def right_fov(self, value) -> None: raise ValueError @property - def bottom_fov(self) -> str | None: + def bottom_fov(self) -> Optional[str]: return self._bottom_fov @bottom_fov.setter @@ -1145,7 +1146,7 @@ def bottom_fov(self, value) -> None: raise ValueError @property - def top_fov(self) -> str | None: + def top_fov(self) -> Optional[str]: return self._top_fov @top_fov.setter @@ -1158,7 +1159,7 @@ def top_fov(self, value) -> None: raise ValueError @property - def near(self) -> str | None: + def near(self) -> Optional[str]: return self._near @near.setter @@ -1171,7 +1172,7 @@ def near(self, value) -> None: raise ValueError @property - def tile_size(self) -> str | None: + def tile_size(self) -> Optional[str]: return self._tile_size @tile_size.setter @@ -1184,7 +1185,7 @@ def tile_size(self, value) -> None: raise ValueError @property - def max_width(self) -> str | None: + def max_width(self) -> Optional[str]: return self._max_width @max_width.setter @@ -1197,7 +1198,7 @@ def max_width(self, value) -> None: raise ValueError @property - def max_height(self) -> str | None: + def max_height(self) -> Optional[str]: return self._max_height @max_height.setter @@ -1210,7 +1211,7 @@ def max_height(self, value) -> None: raise ValueError @property - def grid_origin(self) -> str | None: + def grid_origin(self) -> Optional[str]: return self._grid_origin @grid_origin.setter @@ -1234,7 +1235,7 @@ def point(self, value) -> None: raise ValueError @property - def shape(self) -> str | None: + def shape(self) -> Optional[str]: return self._shape @shape.setter @@ -1408,7 +1409,7 @@ class GroundOverlay(_Overlay): _lat_lon_quad = None @property - def altitude(self) -> str | None: + def altitude(self) -> Optional[str]: return self._altitude @altitude.setter @@ -1432,7 +1433,7 @@ def altitude_mode(self, mode) -> None: self._altitude_mode = "clampToGround" @property - def north(self) -> str | None: + def north(self) -> Optional[str]: return self._north @north.setter @@ -1445,7 +1446,7 @@ def north(self, value) -> None: raise ValueError @property - def south(self) -> str | None: + def south(self) -> Optional[str]: return self._south @south.setter @@ -1458,7 +1459,7 @@ def south(self, value) -> None: raise ValueError @property - def east(self) -> str | None: + def east(self) -> Optional[str]: return self._east @east.setter @@ -1471,7 +1472,7 @@ def east(self, value) -> None: raise ValueError @property - def west(self) -> str | None: + def west(self) -> Optional[str]: return self._west @west.setter @@ -1484,7 +1485,7 @@ def west(self, value) -> None: raise ValueError @property - def rotation(self) -> str | None: + def rotation(self) -> Optional[str]: return self._rotation @rotation.setter From eb79d44ec9e0233476042990b6831d25ebf7bde8 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 11 Nov 2023 18:34:01 +0000 Subject: [PATCH 42/63] Add support for custom name spaces in _XMLObject constructor and fix namespace in MultiTrack #77 #22 --- fastkml/base.py | 2 ++ fastkml/gx.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/fastkml/base.py b/fastkml/base.py index 64e78887..197f4a98 100644 --- a/fastkml/base.py +++ b/fastkml/base.py @@ -143,6 +143,8 @@ def _get_kwargs( strict: bool, ) -> Dict[str, Any]: """Returns a dictionary of kwargs for the class constructor.""" + name_spaces = name_spaces or {} + name_spaces = {**config.NAME_SPACES, **name_spaces} kwargs: Dict[str, Any] = {"ns": ns, "name_spaces": name_spaces} return kwargs diff --git a/fastkml/gx.py b/fastkml/gx.py index 5a81c8b7..b40a07b4 100644 --- a/fastkml/gx.py +++ b/fastkml/gx.py @@ -448,7 +448,7 @@ def _get_kwargs( strict=strict, ) kwargs["tracks"] = cls._get_track_kwargs_from_element( - ns=config.GXNS, + ns=kwargs["name_spaces"].get("gx", ""), element=element, strict=strict, ) From 0b3506c4ed888f765e68796215d0c1f7d0dcc9b2 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 11 Nov 2023 18:37:02 +0000 Subject: [PATCH 43/63] remove flake8-continuation --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5a0dbb62..b5db983c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,6 @@ linting = [ "black", "flake8", "flake8-comments", - "flake8-continuation", "flake8-encodings", "flake8-expression-complexity", "flake8-length", From 33e05e76cf970f6c66798074869fc0f5d204a4c8 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 11 Nov 2023 18:42:18 +0000 Subject: [PATCH 44/63] ignore LIT002 for gx and views --- docs/conf.py | 2 +- tox.ini | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 342f0a47..5d1c1465 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -205,7 +205,7 @@ "index", "FastKML.tex", "FastKML Documentation", - "Christian Ledermann \\& Ian Lee", + r"Christian Ledermann \& Ian Lee", "manual", ), ] diff --git a/tox.ini b/tox.ini index 4145a018..e5dd7dbf 100644 --- a/tox.ini +++ b/tox.ini @@ -34,6 +34,8 @@ ignore= per-file-ignores = tests/*.py: E722,E741,E501,DALL examples/*.py: DALL + fastkml/gx.py: LIT002 + fastkml/views.py: LIT002 enable-extensions=G literal_inline_quotes = double literal_multiline_quotes = double From 61526ab74d9306c65a0d0778773adffb2f47e7df Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 11 Nov 2023 18:46:01 +0000 Subject: [PATCH 45/63] ruff and black reformat --- fastkml/data.py | 20 ++++++++++++++++---- fastkml/geometry.py | 5 ++++- fastkml/gx.py | 10 ++++++++-- fastkml/views.py | 27 ++++++++++++++++++--------- 4 files changed, 46 insertions(+), 16 deletions(-) diff --git a/fastkml/data.py b/fastkml/data.py index 05fd60b0..b54ca159 100644 --- a/fastkml/data.py +++ b/fastkml/data.py @@ -183,7 +183,10 @@ def _get_kwargs( strict: bool, ) -> Dict[str, Any]: kwargs = super()._get_kwargs( - ns=ns, name_spaces=name_spaces, element=element, strict=strict + ns=ns, + name_spaces=name_spaces, + element=element, + strict=strict, ) kwargs["name"] = element.get("name") kwargs["fields"] = cls._get_fields_kwargs_from_element( @@ -251,7 +254,10 @@ def _get_kwargs( strict: bool, ) -> Dict[str, Any]: kwargs = super()._get_kwargs( - ns=ns, name_spaces=name_spaces, element=element, strict=strict + ns=ns, + name_spaces=name_spaces, + element=element, + strict=strict, ) kwargs["name"] = element.get("name") value = element.find(f"{ns}value") @@ -343,7 +349,10 @@ def _get_kwargs( strict: bool, ) -> Dict[str, Any]: kwargs = super()._get_kwargs( - ns=ns, name_spaces=name_spaces, element=element, strict=strict + ns=ns, + name_spaces=name_spaces, + element=element, + strict=strict, ) kwargs["schema_url"] = element.get("schemaUrl") kwargs["data"] = [ @@ -401,7 +410,10 @@ def _get_kwargs( strict: bool, ) -> Dict[str, Any]: kwargs = super()._get_kwargs( - ns=ns, name_spaces=name_spaces, element=element, strict=strict + ns=ns, + name_spaces=name_spaces, + element=element, + strict=strict, ) elements = [] untyped_data = element.findall(f"{ns}Data") diff --git a/fastkml/geometry.py b/fastkml/geometry.py index e9d84f32..df8ae604 100644 --- a/fastkml/geometry.py +++ b/fastkml/geometry.py @@ -317,7 +317,10 @@ def _get_kwargs( strict: bool, ) -> Dict[str, Any]: kwargs = super()._get_kwargs( - ns=ns, name_spaces=name_spaces, element=element, strict=strict + ns=ns, + name_spaces=name_spaces, + element=element, + strict=strict, ) kwargs.update(cls._get_geometry_kwargs(ns=ns, element=element, strict=strict)) kwargs.update( diff --git a/fastkml/gx.py b/fastkml/gx.py index b40a07b4..e0a64e8d 100644 --- a/fastkml/gx.py +++ b/fastkml/gx.py @@ -295,7 +295,10 @@ def _get_kwargs( strict: bool, ) -> Dict[str, Any]: kwargs = super()._get_kwargs( - ns=ns, name_spaces=name_spaces, element=element, strict=strict + ns=ns, + name_spaces=name_spaces, + element=element, + strict=strict, ) kwargs["track_items"] = cls.track_items_kwargs_from_element( ns=ns, @@ -440,7 +443,10 @@ def _get_kwargs( strict: bool, ) -> Dict[str, Any]: kwargs = super()._get_kwargs( - ns=ns, name_spaces=name_spaces, element=element, strict=strict + ns=ns, + name_spaces=name_spaces, + element=element, + strict=strict, ) kwargs["interpolate"] = cls._get_interpolate( ns=ns, diff --git a/fastkml/views.py b/fastkml/views.py index 15659aea..0b09979a 100644 --- a/fastkml/views.py +++ b/fastkml/views.py @@ -202,27 +202,32 @@ def etree_element( element = super().etree_element(precision=precision, verbosity=verbosity) if self.longitude: longitude = config.etree.SubElement( # type: ignore[attr-defined] - element, f"{self.ns}longitude" + element, + f"{self.ns}longitude", ) longitude.text = str(self.longitude) if self.latitude: latitude = config.etree.SubElement( # type: ignore[attr-defined] - element, f"{self.ns}latitude" + element, + f"{self.ns}latitude", ) latitude.text = str(self.latitude) if self.altitude: altitude = config.etree.SubElement( # type: ignore[attr-defined] - element, f"{self.ns}altitude" + element, + f"{self.ns}altitude", ) altitude.text = str(self.altitude) if self.heading: heading = config.etree.SubElement( # type: ignore[attr-defined] - element, f"{self.ns}heading" + element, + f"{self.ns}heading", ) heading.text = str(self.heading) if self.tilt: tilt = config.etree.SubElement( # type: ignore[attr-defined] - element, f"{self.ns}tilt" + element, + f"{self.ns}tilt", ) tilt.text = str(self.tilt) if self.altitude_mode in ( @@ -231,14 +236,16 @@ def etree_element( AltitudeMode.absolute, ): altitude_mode = config.etree.SubElement( # type: ignore[attr-defined] - element, f"{self.ns}altitudeMode" + element, + f"{self.ns}altitudeMode", ) elif self.altitude_mode in ( AltitudeMode.clamp_to_sea_floor, AltitudeMode.relative_to_sea_floor, ): altitude_mode = config.etree.SubElement( # type: ignore[attr-defined] - element, f"{self.name_spaces['gx']}altitudeMode" + element, + f"{self.name_spaces['gx']}altitudeMode", ) altitude_mode.text = self.altitude_mode.value if (self._timespan is not None) and (self._timestamp is not None): @@ -326,7 +333,8 @@ def etree_element( element = super().etree_element(precision=precision, verbosity=verbosity) if self.roll: roll = config.etree.SubElement( # type: ignore[attr-defined] - element, f"{self.ns}roll" + element, + f"{self.ns}roll", ) roll.text = str(self.roll) return element @@ -399,7 +407,8 @@ def etree_element( element = super().etree_element(precision=precision, verbosity=verbosity) if self.range: range_var = config.etree.SubElement( # type: ignore[attr-defined] - element, f"{self.ns}range" + element, + f"{self.ns}range", ) range_var.text = str(self._range) return element From b470a6094390d7f2e5fc27d97d31cfb3041e101d Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 11 Nov 2023 18:53:22 +0000 Subject: [PATCH 46/63] noqa ENC for examples --- examples/UsageExamples.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/UsageExamples.py b/examples/UsageExamples.py index 552a2d7f..b55e6210 100644 --- a/examples/UsageExamples.py +++ b/examples/UsageExamples.py @@ -17,7 +17,7 @@ def print_child_features(element): k = kml.KML() - with open(fname) as kml_file: + with open(fname) as kml_file: # noqa: ENC001 k.from_string(kml_file.read().encode("utf-8")) print_child_features(k) From 2cc4233a5354f2d996b46ad44b65430c0adeb23d Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 11 Nov 2023 20:13:28 +0000 Subject: [PATCH 47/63] Fix TimeStamp and TimeSpan class to use class_from_element method --- fastkml/base.py | 8 +++++- fastkml/kml.py | 18 ++++++++---- fastkml/times.py | 62 +++++++++++++++++++++++++++++++---------- fastkml/views.py | 25 +++++++++++++---- tests/times_test.py | 67 +++++++++++++++++++++++++++++++++++---------- 5 files changed, 138 insertions(+), 42 deletions(-) diff --git a/fastkml/base.py b/fastkml/base.py index 197f4a98..0181b0b7 100644 --- a/fastkml/base.py +++ b/fastkml/base.py @@ -158,7 +158,12 @@ def class_from_element( strict: bool, ) -> "_XMLObject": """Creates an XML object from an etree element.""" - kwargs = cls._get_kwargs(ns=ns, element=element, strict=strict) + kwargs = cls._get_kwargs( + ns=ns, + name_spaces=name_spaces, + element=element, + strict=strict, + ) return cls( **kwargs, ) @@ -186,6 +191,7 @@ def class_from_string( ns = cls._get_ns(ns) return cls.class_from_element( ns=ns, + name_spaces=name_spaces, strict=strict, element=cast( Element, diff --git a/fastkml/kml.py b/fastkml/kml.py index 82564b65..1f1b5a3a 100644 --- a/fastkml/kml.py +++ b/fastkml/kml.py @@ -458,14 +458,20 @@ def from_element(self, element: Element, strict: bool = False) -> None: self.snippet = _snippet timespan = element.find(f"{self.ns}TimeSpan") if timespan is not None: - s = TimeSpan(self.ns) - s.from_element(timespan) - self._timespan = s + self._timespan = TimeSpan.class_from_element( + ns=self.ns, + name_spaces=self.name_spaces, + element=timespan, + strict=strict, + ) timestamp = element.find(f"{self.ns}TimeStamp") if timestamp is not None: - s = TimeStamp(self.ns) - s.from_element(timestamp) - self._timestamp = s + self._timestamp = TimeStamp.class_from_element( + ns=self.ns, + name_spaces=self.name_spaces, + element=timestamp, + strict=strict, + ) atom_link = element.find(f"{atom.NS}link") if atom_link is not None: s = atom.Link() diff --git a/fastkml/times.py b/fastkml/times.py index 57584dbb..b71063eb 100644 --- a/fastkml/times.py +++ b/fastkml/times.py @@ -17,6 +17,8 @@ import re from datetime import date from datetime import datetime +from typing import Any +from typing import Dict from typing import Optional from typing import Union @@ -157,11 +159,12 @@ class TimeStamp(_TimePrimitive): def __init__( self, ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, id: Optional[str] = None, target_id: Optional[str] = None, timestamp: Optional[KmlDateTime] = None, ) -> None: - super().__init__(ns=ns, id=id, target_id=target_id) + super().__init__(ns=ns, name_spaces=name_spaces, id=id, target_id=target_id) self.timestamp = timestamp def etree_element( @@ -177,11 +180,25 @@ def etree_element( when.text = str(self.timestamp) return element - def from_element(self, element: Element) -> None: - super().from_element(element) - when = element.find(f"{self.ns}when") + @classmethod + def _get_kwargs( + cls, + *, + ns: str, + name_spaces: Optional[Dict[str, str]] = None, + element: Element, + strict: bool, + ) -> Dict[str, Any]: + kwargs = super()._get_kwargs( + ns=ns, + name_spaces=name_spaces, + element=element, + strict=strict, + ) + when = element.find(f"{ns}when") if when is not None: - self.timestamp = KmlDateTime.parse(when.text) + kwargs["timestamp"] = KmlDateTime.parse(when.text) + return kwargs class TimeSpan(_TimePrimitive): @@ -192,24 +209,16 @@ class TimeSpan(_TimePrimitive): def __init__( self, ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, id: Optional[str] = None, target_id: Optional[str] = None, begin: Optional[KmlDateTime] = None, end: Optional[KmlDateTime] = None, ) -> None: - super().__init__(ns=ns, id=id, target_id=target_id) + super().__init__(ns=ns, name_spaces=name_spaces, id=id, target_id=target_id) self.begin = begin self.end = end - def from_element(self, element: Element) -> None: - super().from_element(element) - begin = element.find(f"{self.ns}begin") - if begin is not None: - self.begin = KmlDateTime.parse(begin.text) - end = element.find(f"{self.ns}end") - if end is not None: - self.end = KmlDateTime.parse(end.text) - def etree_element( self, precision: Optional[int] = None, @@ -237,3 +246,26 @@ def etree_element( raise ValueError(msg) # TODO test if end > begin return element + + @classmethod + def _get_kwargs( + cls, + *, + ns: str, + name_spaces: Optional[Dict[str, str]] = None, + element: Element, + strict: bool, + ) -> Dict[str, Any]: + kwargs = super()._get_kwargs( + ns=ns, + name_spaces=name_spaces, + element=element, + strict=strict, + ) + begin = element.find(f"{ns}begin") + if begin is not None: + kwargs["begin"] = KmlDateTime.parse(begin.text) + end = element.find(f"{ns}end") + if end is not None: + kwargs["end"] = KmlDateTime.parse(end.text) + return kwargs diff --git a/fastkml/views.py b/fastkml/views.py index 0b09979a..71f2eda8 100644 --- a/fastkml/views.py +++ b/fastkml/views.py @@ -3,6 +3,7 @@ from typing import Optional from typing import SupportsFloat from typing import Union +from typing import cast from fastkml import config from fastkml.base import _BaseObject @@ -185,14 +186,26 @@ def from_element(self, element: Element) -> None: self.altitude_mode = AltitudeMode(altitude_mode.text) timespan = element.find(f"{self.ns}TimeSpan") if timespan is not None: - span = TimeSpan(self.ns) - span.from_element(timespan) - self._timespan = span + self._timespan = cast( + TimeSpan, + TimeSpan.class_from_element( + ns=self.ns, + name_spaces=self.name_spaces, + element=timespan, + strict=False, + ), + ) timestamp = element.find(f"{self.ns}TimeStamp") if timestamp is not None: - stamp = TimeStamp(self.ns) - stamp.from_element(timestamp) - self._timestamp = stamp + self._timestamp = cast( + TimeStamp, + TimeStamp.class_from_element( + ns=self.ns, + name_spaces=self.name_spaces, + element=timestamp, + strict=False, + ), + ) def etree_element( self, diff --git a/tests/times_test.py b/tests/times_test.py index 33081770..5fc46851 100644 --- a/tests/times_test.py +++ b/tests/times_test.py @@ -151,7 +151,13 @@ def test_parse_datetime_with_tz(self) -> None: assert dt.resolution == DateTimeResolution.datetime assert dt.dt == datetime.datetime( - 1997, 7, 16, 7, 30, 15, tzinfo=tzoffset(None, 3600), + 1997, + 7, + 16, + 7, + 30, + 15, + tzinfo=tzoffset(None, 3600), ) def test_parse_datetime_with_tz_no_colon(self) -> None: @@ -159,7 +165,13 @@ def test_parse_datetime_with_tz_no_colon(self) -> None: assert dt.resolution == DateTimeResolution.datetime assert dt.dt == datetime.datetime( - 1997, 7, 16, 7, 30, 15, tzinfo=tzoffset(None, 3600), + 1997, + 7, + 16, + 7, + 30, + 15, + tzinfo=tzoffset(None, 3600), ) def test_parse_datetime_no_tz(self) -> None: @@ -284,44 +296,55 @@ def test_feature_timespan_stamp(self) -> None: # are allowed not both pytest.raises(ValueError, f.to_string) - def test_read_timestamp(self) -> None: - ts = kml.TimeStamp(ns="") + def test_read_timestamp_year(self) -> None: doc = """ 1997 """ - ts.from_string(doc) + ts = kml.TimeStamp.class_from_string(doc, ns="") + assert ts.timestamp.resolution == DateTimeResolution.year assert ts.timestamp.dt == datetime.datetime(1997, 1, 1, 0, 0, tzinfo=tzutc()) + + def test_read_timestamp_year_month(self) -> None: doc = """ 1997-07 """ - ts.from_string(doc) + ts = kml.TimeStamp.class_from_string(doc, ns="") + assert ts.timestamp.resolution == DateTimeResolution.year_month assert ts.timestamp.dt == datetime.datetime(1997, 7, 1, 0, 0, tzinfo=tzutc()) + + def test_read_timestamp_ym_no_hyphen(self) -> None: doc = """ 199808 """ - ts.from_string(doc) + ts = kml.TimeStamp.class_from_string(doc, ns="") + assert ts.timestamp.resolution == DateTimeResolution.year_month assert ts.timestamp.dt == datetime.datetime(1998, 8, 1, 0, 0, tzinfo=tzutc()) + + def test_read_timestamp_ymd(self) -> None: doc = """ 1997-07-16 """ - ts.from_string(doc) + ts = kml.TimeStamp.class_from_string(doc, ns="") + assert ts.timestamp.resolution == DateTimeResolution.date assert ts.timestamp.dt == datetime.datetime(1997, 7, 16, 0, 0, tzinfo=tzutc()) + + def test_read_timestamp_utc(self) -> None: # dateTime (YYYY-MM-DDThh:mm:ssZ) # Here, T is the separator between the calendar and the hourly notation # of time, and Z indicates UTC. (Seconds are required.) @@ -331,25 +354,40 @@ def test_read_timestamp(self) -> None: """ - ts.from_string(doc) + ts = kml.TimeStamp.class_from_string(doc, ns="") + assert ts.timestamp.resolution == DateTimeResolution.datetime assert ts.timestamp.dt == datetime.datetime( - 1997, 7, 16, 7, 30, 15, tzinfo=tzutc(), + 1997, + 7, + 16, + 7, + 30, + 15, + tzinfo=tzutc(), ) + + def test_read_timestamp_utc_offset(self) -> None: doc = """ 1997-07-16T10:30:15+03:00 """ - ts.from_string(doc) + ts = kml.TimeStamp.class_from_string(doc, ns="") + assert ts.timestamp.resolution == DateTimeResolution.datetime assert ts.timestamp.dt == datetime.datetime( - 1997, 7, 16, 10, 30, 15, tzinfo=tzoffset(None, 10800), + 1997, + 7, + 16, + 10, + 30, + 15, + tzinfo=tzoffset(None, 10800), ) def test_read_timespan(self) -> None: - ts = kml.TimeSpan(ns="") doc = """ 1876-08-01 @@ -357,7 +395,8 @@ def test_read_timespan(self) -> None: """ - ts.from_string(doc) + ts = kml.TimeSpan.class_from_string(doc, ns="") + assert ts.begin.resolution == DateTimeResolution.date assert ts.begin.dt == datetime.datetime(1876, 8, 1, 0, 0, tzinfo=tzutc()) assert ts.end.resolution == DateTimeResolution.datetime From 19571210eb6b65016db826a27da4ce0b5cfb3fae Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 11 Nov 2023 20:14:52 +0000 Subject: [PATCH 48/63] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- examples/UsageExamples.py | 2 +- tests/atom_test.py | 4 +++- tests/base_test.py | 3 ++- tests/geometries/point_test.py | 3 ++- tests/gx_test.py | 44 +++++++++++++++++++++++++++++----- 5 files changed, 46 insertions(+), 10 deletions(-) diff --git a/examples/UsageExamples.py b/examples/UsageExamples.py index b55e6210..552a2d7f 100644 --- a/examples/UsageExamples.py +++ b/examples/UsageExamples.py @@ -17,7 +17,7 @@ def print_child_features(element): k = kml.KML() - with open(fname) as kml_file: # noqa: ENC001 + with open(fname) as kml_file: k.from_string(kml_file.read().encode("utf-8")) print_child_features(k) diff --git a/tests/atom_test.py b/tests/atom_test.py index d059e1c6..533448a1 100644 --- a/tests/atom_test.py +++ b/tests/atom_test.py @@ -82,7 +82,9 @@ def test_atom_person_ns(self) -> None: def test_atom_author(self) -> None: a = atom.Author( - name="Nobody", uri="http://localhost", email="cl@donotreply.com", + name="Nobody", + uri="http://localhost", + email="cl@donotreply.com", ) serialized = a.to_string() diff --git a/tests/base_test.py b/tests/base_test.py index 78eb7bba..0ab1b2fa 100644 --- a/tests/base_test.py +++ b/tests/base_test.py @@ -43,7 +43,8 @@ def test_to_str_empty_ns(self) -> None: obj.__name__ = "test" assert obj.to_string().replace(" ", "").replace( - "\n", "", + "\n", + "", ) == ''.replace(" ", "") def test_from_string(self) -> None: diff --git a/tests/geometries/point_test.py b/tests/geometries/point_test.py index dde8ce3a..e2c73c77 100644 --- a/tests/geometries/point_test.py +++ b/tests/geometries/point_test.py @@ -54,7 +54,8 @@ def test_to_string_empty_geometry(self) -> None: point = Point(geometry=geo.Point(None, None)) # type: ignore[arg-type] with pytest.raises( - KMLWriteError, match=r"Invalid dimensions in coordinates '\(\(\),\)'", + KMLWriteError, + match=r"Invalid dimensions in coordinates '\(\(\),\)'", ): point.to_string() diff --git a/tests/gx_test.py b/tests/gx_test.py index 1212b0bf..0ba0da87 100644 --- a/tests/gx_test.py +++ b/tests/gx_test.py @@ -95,14 +95,24 @@ def test_multitrack(self) -> None: track_items=[ TrackItem( when=datetime.datetime( - 2020, 1, 1, 0, 0, tzinfo=tzutc(), + 2020, + 1, + 1, + 0, + 0, + tzinfo=tzutc(), ), coord=geo.Point(0.0, 0.0), angle=None, ), TrackItem( when=datetime.datetime( - 2020, 1, 1, 0, 10, tzinfo=tzutc(), + 2020, + 1, + 1, + 0, + 10, + tzinfo=tzutc(), ), coord=geo.Point(1.0, 0.0), angle=None, @@ -119,14 +129,24 @@ def test_multitrack(self) -> None: track_items=[ TrackItem( when=datetime.datetime( - 2020, 1, 1, 0, 10, tzinfo=tzutc(), + 2020, + 1, + 1, + 0, + 10, + tzinfo=tzutc(), ), coord=geo.Point(0.0, 1.0), angle=None, ), TrackItem( when=datetime.datetime( - 2020, 1, 1, 0, 20, tzinfo=tzutc(), + 2020, + 1, + 1, + 0, + 20, + tzinfo=tzutc(), ), coord=geo.Point(1.0, 1.0), angle=None, @@ -384,14 +404,26 @@ def test_multitrack(self) -> None: track_items=[ TrackItem( when=datetime.datetime( - 2010, 5, 28, 2, 2, 55, tzinfo=tzutc(), + 2010, + 5, + 28, + 2, + 2, + 55, + tzinfo=tzutc(), ), coord=geo.Point(-122.203451, 37.374706, 141.800003), angle=Angle(heading=1.0, tilt=2.0, roll=3.0), ), TrackItem( when=datetime.datetime( - 2010, 5, 28, 2, 2, 56, tzinfo=tzutc(), + 2010, + 5, + 28, + 2, + 2, + 56, + tzinfo=tzutc(), ), coord=geo.Point(-122.203329, 37.37478, 141.199997), angle=Angle(heading=1.0, tilt=2.0, roll=3.0), From 046dd3f566073554ff476b65904833b5398b6213 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 11 Nov 2023 20:22:27 +0000 Subject: [PATCH 49/63] Fix encoding issue in KML file reading --- examples/UsageExamples.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/UsageExamples.py b/examples/UsageExamples.py index 552a2d7f..bf4a44f2 100644 --- a/examples/UsageExamples.py +++ b/examples/UsageExamples.py @@ -4,7 +4,7 @@ def print_child_features(element): - """Prints the name of every child node of the given element, recursively""" + """Prints the name of every child node of the given element, recursively.""" if not getattr(element, "features", None): return for feature in element.features(): @@ -17,7 +17,7 @@ def print_child_features(element): k = kml.KML() - with open(fname) as kml_file: + with open(fname, encoding="utf-8") as kml_file: k.from_string(kml_file.read().encode("utf-8")) print_child_features(k) From 6638bec58d77234bea7d7d76e4a21f53e389ef44 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 11 Nov 2023 21:06:26 +0000 Subject: [PATCH 50/63] Refactor views.py to use classmethod for parsing XML elements --- fastkml/views.py | 157 ++++++++++++++++++++++++++++---------------- tests/views_test.py | 7 +- 2 files changed, 102 insertions(+), 62 deletions(-) diff --git a/fastkml/views.py b/fastkml/views.py index 71f2eda8..dac97d72 100644 --- a/fastkml/views.py +++ b/fastkml/views.py @@ -1,4 +1,5 @@ import logging +from typing import Any from typing import Dict from typing import Optional from typing import SupportsFloat @@ -162,51 +163,6 @@ def altitude_mode(self) -> AltitudeMode: def altitude_mode(self, mode: AltitudeMode) -> None: self._altitude_mode = mode - def from_element(self, element: Element) -> None: - super().from_element(element) - longitude = element.find(f"{self.ns}longitude") - if longitude is not None: - self.longitude = float(longitude.text) - latitude = element.find(f"{self.ns}latitude") - if latitude is not None: - self.latitude = float(latitude.text) - altitude = element.find(f"{self.ns}altitude") - if altitude is not None: - self.altitude = float(altitude.text) - heading = element.find(f"{self.ns}heading") - if heading is not None: - self.heading = float(heading.text) - tilt = element.find(f"{self.ns}tilt") - if tilt is not None: - self.tilt = float(tilt.text) - altitude_mode = element.find(f"{self.ns}altitudeMode") - if altitude_mode is None: - altitude_mode = element.find(f"{self.name_spaces['gx']}altitudeMode") - if altitude_mode is not None: - self.altitude_mode = AltitudeMode(altitude_mode.text) - timespan = element.find(f"{self.ns}TimeSpan") - if timespan is not None: - self._timespan = cast( - TimeSpan, - TimeSpan.class_from_element( - ns=self.ns, - name_spaces=self.name_spaces, - element=timespan, - strict=False, - ), - ) - timestamp = element.find(f"{self.ns}TimeStamp") - if timestamp is not None: - self._timestamp = cast( - TimeStamp, - TimeStamp.class_from_element( - ns=self.ns, - name_spaces=self.name_spaces, - element=timestamp, - strict=False, - ), - ) - def etree_element( self, precision: Optional[int] = None, @@ -273,6 +229,65 @@ def etree_element( # TODO: # TODO: + @classmethod + def _get_kwargs( + cls, + *, + ns: str, + name_spaces: Optional[Dict[str, str]] = None, + element: Element, + strict: bool, + ) -> Dict[str, Any]: + kwargs = super()._get_kwargs( + ns=ns, + name_spaces=name_spaces, + element=element, + strict=strict, + ) + longitude = element.find(f"{ns}longitude") + if longitude is not None: + kwargs["longitude"] = float(longitude.text) + latitude = element.find(f"{ns}latitude") + if latitude is not None: + kwargs["latitude"] = float(latitude.text) + altitude = element.find(f"{ns}altitude") + if altitude is not None: + kwargs["altitude"] = float(altitude.text) + heading = element.find(f"{ns}heading") + if heading is not None: + kwargs["heading"] = float(heading.text) + tilt = element.find(f"{ns}tilt") + if tilt is not None: + kwargs["tilt"] = float(tilt.text) + altitude_mode = element.find(f"{ns}altitudeMode") + if altitude_mode is None: + altitude_mode = element.find(f"{kwargs['name_spaces']['gx']}altitudeMode") + if altitude_mode is not None: + kwargs["altitude_mode"] = AltitudeMode(altitude_mode.text) + timespan = element.find(f"{ns}TimeSpan") + if timespan is not None: + kwargs["time_primitive"] = cast( + TimeSpan, + TimeSpan.class_from_element( + ns=ns, + name_spaces=name_spaces, + element=timespan, + strict=strict, + ), + ) + timestamp = element.find(f"{ns}TimeStamp") + if timestamp is not None: + kwargs["time_primitive"] = cast( + TimeStamp, + TimeStamp.class_from_element( + ns=ns, + name_spaces=name_spaces, + element=timestamp, + strict=strict, + ), + ) + return kwargs + class Camera(_AbstractView): """ @@ -332,12 +347,6 @@ def __init__( ) self._roll = roll - def from_element(self, element: Element) -> None: - super().from_element(element) - roll = element.find(f"{self.ns}roll") - if roll is not None: - self.roll = float(roll.text) - def etree_element( self, precision: Optional[int] = None, @@ -360,6 +369,26 @@ def roll(self) -> Optional[float]: def roll(self, value: float) -> None: self._roll = value + @classmethod + def _get_kwargs( + cls, + *, + ns: str, + name_spaces: Optional[Dict[str, str]] = None, + element: Element, + strict: bool, + ) -> Dict[str, Any]: + kwargs = super()._get_kwargs( + ns=ns, + name_spaces=name_spaces, + element=element, + strict=strict, + ) + roll = element.find(f"{ns}roll") + if roll is not None: + kwargs["roll"] = float(roll.text) + return kwargs + class LookAt(_AbstractView): __name__ = "LookAt" @@ -406,12 +435,6 @@ def range(self) -> Optional[float]: def range(self, value: float) -> None: self._range = value - def from_element(self, element: Element) -> None: - super().from_element(element) - range_var = element.find(f"{self.ns}range") - if range_var is not None: - self.range = float(range_var.text) - def etree_element( self, precision: Optional[int] = None, @@ -426,6 +449,26 @@ def etree_element( range_var.text = str(self._range) return element + @classmethod + def _get_kwargs( + cls, + *, + ns: str, + name_spaces: Optional[Dict[str, str]] = None, + element: Element, + strict: bool, + ) -> Dict[str, Any]: + kwargs = super()._get_kwargs( + ns=ns, + name_spaces=name_spaces, + element=element, + strict=strict, + ) + range_var = element.find(f"{ns}range") + if range_var is not None: + kwargs["range"] = float(range_var.text) + return kwargs + __all__ = [ "Camera", diff --git a/tests/views_test.py b/tests/views_test.py index ae51cb54..4a9cd8f2 100644 --- a/tests/views_test.py +++ b/tests/views_test.py @@ -86,9 +86,8 @@ def test_camera_read(self) -> None: "relativeToGround" "" ) - camera = views.Camera() - camera.from_string(camera_xml) + camera = views.Camera.class_from_string(camera_xml) assert camera.heading == 10 assert camera.tilt == 20 @@ -157,9 +156,7 @@ def test_look_at_read(self) -> None: "30" "" ) - look_at = views.LookAt() - - look_at.from_string(look_at_xml) + look_at = views.LookAt.class_from_string(look_at_xml) assert look_at.heading == 10 assert look_at.tilt == 20 From 4924eb334fd6fa4d7574c18c237bb46777823ca0 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 11 Nov 2023 22:04:40 +0000 Subject: [PATCH 51/63] Refactor StyleUrl class to use classmethod for instantiation --- fastkml/kml.py | 9 ++++++--- fastkml/styles.py | 46 ++++++++++++++++++++++++++++++++++++-------- tests/styles_test.py | 4 +--- 3 files changed, 45 insertions(+), 14 deletions(-) diff --git a/fastkml/kml.py b/fastkml/kml.py index 1f1b5a3a..74ca8402 100644 --- a/fastkml/kml.py +++ b/fastkml/kml.py @@ -447,9 +447,12 @@ def from_element(self, element: Element, strict: bool = False) -> None: self.append_style(s) style_url = element.find(f"{self.ns}styleUrl") if style_url is not None: - s = StyleUrl(self.ns) - s.from_element(style_url) - self._style_url = s + self._style_url = StyleUrl.class_from_element( + ns=self.ns, + name_spaces=self.name_spaces, + element=style_url, + strict=strict, + ) snippet = element.find(f"{self.ns}Snippet") if snippet is not None: _snippet = {"text": snippet.text} diff --git a/fastkml/styles.py b/fastkml/styles.py index c18c739e..749cd191 100644 --- a/fastkml/styles.py +++ b/fastkml/styles.py @@ -21,6 +21,7 @@ """ import logging +from typing import Any from typing import Dict from typing import Iterable from typing import Iterator @@ -28,6 +29,7 @@ from typing import Optional from typing import Type from typing import Union +from typing import cast from typing_extensions import TypedDict @@ -72,9 +74,23 @@ def etree_element( logger.warning("StyleUrl is missing required url.") return element - def from_element(self, element: Element) -> None: - super().from_element(element) - self.url = element.text + @classmethod + def _get_kwargs( + cls, + *, + ns: str, + name_spaces: Optional[Dict[str, str]] = None, + element: Element, + strict: bool, + ) -> Dict[str, Any]: + kwargs = super()._get_kwargs( + ns=ns, + name_spaces=name_spaces, + element=element, + strict=strict, + ) + kwargs["url"] = element.text + return kwargs class _StyleSelector(_BaseObject): @@ -652,7 +668,7 @@ def __init__( self.normal = normal self.highlight = highlight - def from_element(self, element: Element) -> None: + def from_element(self, element: Element, strict: bool = False) -> None: super().from_element(element) pairs = element.findall(f"{self.ns}Pair") for pair in pairs: @@ -666,8 +682,15 @@ def from_element(self, element: Element) -> None: highlight = Style(self.ns) highlight.from_element(style) elif style_url is not None: - highlight = StyleUrl(self.ns) # type: ignore[assignment] - highlight.from_element(style_url) + highlight = cast( # type: ignore[assignment] + StyleUrl, + StyleUrl.class_from_element( + ns=self.ns, + name_spaces=self.name_spaces, + element=style_url, + strict=strict, + ), + ) else: raise ValueError self.highlight = highlight @@ -676,8 +699,15 @@ def from_element(self, element: Element) -> None: normal = Style(self.ns) normal.from_element(style) elif style_url is not None: - normal = StyleUrl(self.ns) # type: ignore[assignment] - normal.from_element(style_url) + normal = cast( # type: ignore[assignment] + StyleUrl, + StyleUrl.class_from_element( + ns=self.ns, + name_spaces=self.name_spaces, + element=style_url, + strict=strict, + ), + ) else: raise ValueError self.normal = normal diff --git a/tests/styles_test.py b/tests/styles_test.py index f904e9b6..d218153c 100644 --- a/tests/styles_test.py +++ b/tests/styles_test.py @@ -35,9 +35,7 @@ def test_style_url(self) -> None: assert ">#style-0" in serialized def test_style_url_read(self) -> None: - url = styles.StyleUrl() - - url.from_string( + url = styles.StyleUrl.class_from_string( '#style-0', ) From 315b88396e4f4c0fedad027beb6f0e358013b9de Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 11 Nov 2023 23:15:05 +0000 Subject: [PATCH 52/63] Improve doc string --- fastkml/styles.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fastkml/styles.py b/fastkml/styles.py index 749cd191..85ed2e8c 100644 --- a/fastkml/styles.py +++ b/fastkml/styles.py @@ -43,8 +43,9 @@ class StyleUrl(_BaseObject): """ - URL of a