From a1dd8935da6272d94e52fe90732da698c9b59767 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Mon, 20 Nov 2023 21:59:02 +0000 Subject: [PATCH] Refactor code and update dependencies --- .pre-commit-config.yaml | 5 - fastkml/atom.py | 212 ++++++++++++++++++++-------------------- fastkml/base.py | 46 +++------ fastkml/geometry.py | 24 +++-- fastkml/helpers.py | 157 ----------------------------- fastkml/kml.py | 20 ++-- fastkml/types.py | 15 +-- tests/atom_test.py | 22 ++--- tests/oldunit_test.py | 3 +- 9 files changed, 167 insertions(+), 337 deletions(-) delete mode 100644 fastkml/helpers.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 706a578c..6d9169f0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,11 +36,6 @@ repos: rev: v0.3.1 hooks: - id: absolufy-imports - - repo: https://github.com/hakancelikdev/unimport - rev: 1.1.0 - hooks: - - id: unimport - args: [--remove, --include-star-import, --ignore-init, --gitignore] - repo: https://github.com/asottile/pyupgrade rev: v3.15.0 hooks: diff --git a/fastkml/atom.py b/fastkml/atom.py index 44efe964..d497d271 100644 --- a/fastkml/atom.py +++ b/fastkml/atom.py @@ -33,19 +33,15 @@ import logging import re +from typing import Any +from typing import Dict from typing import Optional -from typing import Tuple +from fastkml import config from fastkml.base import _XMLObject from fastkml.config import ATOMNS as NS from fastkml.enums import Verbosity -from fastkml.helpers import o_from_attr -from fastkml.helpers import o_from_subelement_text -from fastkml.helpers import o_int_from_attr -from fastkml.helpers import o_to_attr -from fastkml.helpers import o_to_subelement_text from fastkml.types import Element -from fastkml.types import KmlObjectMap logger = logging.getLogger(__name__) regex = r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)" @@ -68,61 +64,10 @@ class Link(_XMLObject): __name__ = "link" - kml_object_mapping: Tuple[KmlObjectMap, ...] = ( - { - "kml_attr": "href", - "obj_attr": "href", - "from_kml": o_from_attr, - "to_kml": o_to_attr, - "required": True, - "validator": None, - }, - { - "kml_attr": "rel", - "obj_attr": "rel", - "from_kml": o_from_attr, - "to_kml": o_to_attr, - "required": False, - "validator": None, - }, - { - "kml_attr": "type", - "obj_attr": "type", - "from_kml": o_from_attr, - "to_kml": o_to_attr, - "required": False, - "validator": None, - }, - { - "kml_attr": "hreflang", - "obj_attr": "hreflang", - "from_kml": o_from_attr, - "to_kml": o_to_attr, - "required": False, - "validator": None, - }, - { - "kml_attr": "title", - "obj_attr": "title", - "from_kml": o_from_attr, - "to_kml": o_to_attr, - "required": False, - "validator": None, - }, - { - "kml_attr": "length", - "obj_attr": "length", - "from_kml": o_int_from_attr, - "to_kml": o_to_attr, - "required": False, - "validator": None, - }, - ) - - href = None + href: Optional[str] # href is the URI of the referenced resource - rel = None + rel: Optional[str] # rel contains a single link relationship type. # It can be a full URI, or one of the following predefined values # (default=alternate): @@ -134,21 +79,22 @@ class Link(_XMLObject): # self: the feed itself. # via: the source of the information provided in the entry. - type = None + type: Optional[str] # indicates the media type of the resource - hreflang = None + hreflang: Optional[str] # indicates the language of the referenced resource - title = None + title: Optional[str] # human readable information about the link - length = None + length: Optional[int] # the length of the resource, in bytes def __init__( self, ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, href: Optional[str] = None, rel: Optional[str] = None, type: Optional[str] = None, @@ -156,7 +102,7 @@ def __init__( title: Optional[str] = None, length: Optional[int] = None, ) -> None: - self.ns: str = NS if ns is None else ns + super().__init__(ns=ns, name_spaces=name_spaces) self.href = href self.rel = rel self.type = type @@ -177,15 +123,51 @@ def __repr__(self) -> str: ")" ) - def from_element(self, element: Element) -> None: - super().from_element(element) - def etree_element( self, precision: Optional[int] = None, verbosity: Verbosity = Verbosity.normal, ) -> Element: - return super().etree_element(precision=precision, verbosity=verbosity) + element = super().etree_element(precision=precision, verbosity=verbosity) + if self.href: + element.set("href", self.href) + else: + logger.warning("required attribute href missing") + if self.rel: + element.set("rel", self.rel) + if self.type: + element.set("type", self.type) + if self.hreflang: + element.set("hreflang", self.hreflang) + if self.title: + element.set("title", self.title) + if self.length: + element.set("length", str(self.length)) + 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, + ) + kwargs["href"] = element.get("href") + kwargs["rel"] = element.get("rel") + kwargs["type"] = element.get("type") + kwargs["hreflang"] = element.get("hreflang") + kwargs["title"] = element.get("title") + length = element.get("length") + kwargs["length"] = int(length) if length else None + return kwargs class _Person(_XMLObject): @@ -196,50 +178,25 @@ class _Person(_XMLObject): """ __name__ = "" - kml_object_mapping: Tuple[KmlObjectMap, ...] = ( - { - "kml_attr": "name", - "obj_attr": "name", - "from_kml": o_from_subelement_text, - "to_kml": o_to_subelement_text, - "required": True, - "validator": None, - }, - { - "kml_attr": "uri", - "obj_attr": "uri", - "from_kml": o_from_subelement_text, - "to_kml": o_to_subelement_text, - "required": False, - "validator": None, - }, - { - "kml_attr": "email", - "obj_attr": "email", - "from_kml": o_from_subelement_text, - "to_kml": o_to_subelement_text, - "required": False, - "validator": check_email, - }, - ) - - name: Optional[str] = None + + name: Optional[str] # conveys a human-readable name for the person. - uri: Optional[str] = None + uri: Optional[str] # contains a home page for the person. - email: Optional[str] = None + email: Optional[str] # contains an email address for the person. def __init__( self, ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, name: Optional[str] = None, uri: Optional[str] = None, email: Optional[str] = None, ) -> None: - self.ns: str = NS if ns is None else ns + super().__init__(ns=ns, name_spaces=name_spaces) self.name = name self.uri = uri self.email = email @@ -259,10 +216,55 @@ def etree_element( precision: Optional[int] = None, verbosity: Verbosity = Verbosity.normal, ) -> Element: - return super().etree_element(precision=precision, verbosity=verbosity) - - def from_element(self, element: Element) -> None: - super().from_element(element) + self.__name__ = self.__class__.__name__.lower() + element = super().etree_element(precision=precision, verbosity=verbosity) + if self.name: + name = config.etree.SubElement( # type: ignore[attr-defined] + element, + f"{self.ns}name", + ) + name.text = self.name + else: + logger.warning("No Name for person defined") + if self.uri: + uri = config.etree.SubElement( # type: ignore[attr-defined] + element, + f"{self.ns}uri", + ) + uri.text = self.uri + if self.email and check_email(self.email): + email = config.etree.SubElement( # type: ignore[attr-defined] + element, + f"{self.ns}email", + ) + email.text = self.email + 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, + ) + name = element.find(f"{ns}name") + if name is not None: + kwargs["name"] = name.text + uri = element.find(f"{ns}uri") + if uri is not None: + kwargs["uri"] = uri.text + email = element.find(f"{ns}email") + if email is not None: + kwargs["email"] = email.text + return kwargs class Author(_Person): @@ -285,4 +287,4 @@ class Contributor(_Person): __name__ = "contributor" -__all__ = ["Author", "Contributor", "Link"] +__all__ = ["Author", "Contributor", "Link", "NS"] diff --git a/fastkml/base.py b/fastkml/base.py index 0181b0b7..49e519b9 100644 --- a/fastkml/base.py +++ b/fastkml/base.py @@ -19,15 +19,11 @@ from typing import Any from typing import Dict from typing import Optional -from typing import Tuple from typing import cast from fastkml import config from fastkml.enums import Verbosity -from fastkml.helpers import o_from_attr -from fastkml.helpers import o_to_attr from fastkml.types import Element -from fastkml.types import KmlObjectMap logger = logging.getLogger(__name__) @@ -39,7 +35,6 @@ class _XMLObject: _node_name: str = "" __name__ = "" name_spaces: Dict[str, str] - kml_object_mapping: Tuple[KmlObjectMap, ...] = () def __init__( self, @@ -72,8 +67,6 @@ def etree_element( raise NotImplementedError( msg, ) - for mapping in self.kml_object_mapping: - mapping["to_kml"](self, element, **mapping) return element def from_element(self, element: Element) -> None: @@ -86,8 +79,6 @@ def from_element(self, element: Element) -> None: if f"{self.ns}{self.__name__}" != element.tag: 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) def from_string(self, xml_string: str) -> None: """ @@ -118,7 +109,9 @@ def to_string( ), encoding="UTF-8", pretty_print=prettyprint, - ).decode("UTF-8"), + ).decode( + "UTF-8", + ), ) except TypeError: return cast( @@ -126,7 +119,9 @@ def to_string( config.etree.tostring( # type: ignore[attr-defined] self.etree_element(), encoding="UTF-8", - ).decode("UTF-8"), + ).decode( + "UTF-8", + ), ) @classmethod @@ -216,24 +211,6 @@ class _BaseObject(_XMLObject): id = None target_id = None - kml_object_mapping: Tuple[KmlObjectMap, ...] = ( - { - "kml_attr": "id", - "obj_attr": "id", - "from_kml": o_from_attr, - "to_kml": o_to_attr, - "required": False, - "validator": None, - }, - { - "kml_attr": "targetId", - "obj_attr": "target_id", - "from_kml": o_from_attr, - "to_kml": o_to_attr, - "required": False, - "validator": None, - }, - ) def __init__( self, @@ -268,11 +245,20 @@ def etree_element( verbosity: Verbosity = Verbosity.normal, ) -> Element: """Return the KML Object as an Element.""" - return super().etree_element(precision=precision, verbosity=verbosity) + element = super().etree_element(precision=precision, verbosity=verbosity) + if self.id: + element.set("id", self.id) + if self.target_id: + element.set("targetId", self.target_id) + return element def from_element(self, element: Element) -> None: """Load the KML Object from an Element.""" super().from_element(element) + if element.get("id"): + self.id = element.get("id") + if element.get("targetId"): + self.target_id = element.get("targetId") @classmethod def _get_id(cls, element: Element, strict: bool) -> str: diff --git a/fastkml/geometry.py b/fastkml/geometry.py index df8ae604..ef251ffb 100644 --- a/fastkml/geometry.py +++ b/fastkml/geometry.py @@ -380,7 +380,9 @@ def _get_geometry( error = config.etree.tostring( # type: ignore[attr-defined] element, encoding="UTF-8", - ).decode("UTF-8") + ).decode( + "UTF-8", + ) msg = f"Invalid coordinates in {error}" raise KMLParseError(msg) from e @@ -436,7 +438,9 @@ def _get_geometry( error = config.etree.tostring( # type: ignore[attr-defined] element, encoding="UTF-8", - ).decode("UTF-8") + ).decode( + "UTF-8", + ) msg = f"Invalid coordinates in {error}" raise KMLParseError(msg) from e @@ -480,7 +484,9 @@ def _get_geometry( error = config.etree.tostring( # type: ignore[attr-defined] element, encoding="UTF-8", - ).decode("UTF-8") + ).decode( + "UTF-8", + ) msg = f"Invalid coordinates in {error}" raise KMLParseError(msg) from e @@ -554,7 +560,9 @@ def _get_geometry(cls, *, ns: str, element: Element, strict: bool) -> geo.Polygo error = config.etree.tostring( # type: ignore[attr-defined] element, encoding="UTF-8", - ).decode("UTF-8") + ).decode( + "UTF-8", + ) msg = f"Missing outerBoundaryIs in {error}" raise KMLParseError(msg) outer_ring = outer_boundary.find(f"{ns}LinearRing") @@ -562,7 +570,9 @@ def _get_geometry(cls, *, ns: str, element: Element, strict: bool) -> geo.Polygo error = config.etree.tostring( # type: ignore[attr-defined] element, encoding="UTF-8", - ).decode("UTF-8") + ).decode( + "UTF-8", + ) msg = f"Missing LinearRing in {error}" raise KMLParseError(msg) exterior = LinearRing._get_geometry(ns=ns, element=outer_ring, strict=strict) @@ -573,7 +583,9 @@ def _get_geometry(cls, *, ns: str, element: Element, strict: bool) -> geo.Polygo error = config.etree.tostring( # type: ignore[attr-defined] element, encoding="UTF-8", - ).decode("UTF-8") + ).decode( + "UTF-8", + ) msg = f"Missing LinearRing in {error}" raise KMLParseError(msg) interiors.append( diff --git a/fastkml/helpers.py b/fastkml/helpers.py deleted file mode 100644 index 5c678a00..00000000 --- a/fastkml/helpers.py +++ /dev/null @@ -1,157 +0,0 @@ -# Copyright (C) 2020 Christian Ledermann -# -# This library is free software; you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by the Free -# Software Foundation; either version 2.1 of the License, or (at your option) -# any later version. -# -# This library is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this library; if not, write to the Free Software Foundation, Inc., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -"""Helper functions and classes.""" -import logging -from typing import Any -from typing import Callable -from typing import Optional - -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__) - - -def o_to_attr( - obj: object, - element: Element, - kml_attr: str, - obj_attr: str, - required: bool, - **kwargs: Any, -) -> None: - """Set an attribute on an KML Element from an object attribute.""" - attribute = getattr(obj, obj_attr) - if attribute: - element.set(kml_attr, str(attribute)) - elif required: - logger.warning( - "Required attribute '%s' for '%s' missing.", - obj_attr, - obj.__class__.__name__, - ) - - -def o_from_attr( - obj: object, - element: Element, - kml_attr: str, - obj_attr: str, - required: bool, - **kwargs: Any, -) -> None: - """Set an attribute on self from an KML attribute.""" - attribute = element.get(kml_attr) - if attribute: - setattr(obj, obj_attr, attribute) - elif required: - logger.warning( - "Required attribute '%s' for '%s' missing.", - kml_attr, - obj.__class__.__name__, - ) - - -def o_int_from_attr( - obj: object, - element: Element, - kml_attr: str, - obj_attr: str, - required: bool, - **kwargs: Any, -) -> None: - """Set an attribute on self from an KML attribute.""" - try: - attribute = int(element.get(kml_attr)) - except (ValueError, TypeError): - attribute = None - if attribute is not None: - setattr(obj, obj_attr, attribute) - elif required: - logger.warning( - "Required attribute '%s' for '%s' missing.", - kml_attr, - obj.__class__.__name__, - ) - - -def o_from_subelement_text( - obj: object, - element: Element, - kml_attr: str, - obj_attr: str, - required: bool, - validator: Optional[Callable[..., bool]] = None, - **kwargs: Any, -) -> None: - """Set an attribute on self from the text of a SubElement.""" - elem = element.find(f"{obj.ns}{kml_attr}") # type: ignore[attr-defined] - if elem is not None: - if validator is not None and not validator(elem.text): - logger.warning( - "Invalid value for attribute '%s' for '%s'", - kml_attr, - obj.__class__.__name__, - ) - else: - setattr(obj, obj_attr, elem.text) - elif required: - logger.warning( - "Required attribute '%s' for '%s' missing.", - kml_attr, - obj.__class__.__name__, - ) - - -def o_to_subelement_text( - obj: object, - element: Element, - kml_attr: str, - obj_attr: str, - required: bool, - validator: Optional[Callable[..., bool]] = None, - **kwargs: Any, -) -> None: - """Set the text of a SubElement from an object attribute.""" - attribute = getattr(obj, obj_attr) - if attribute: - if validator is not None and not validator(attribute): - logger.warning( - "Invalid value for attribute '%s' for '%s'", - obj_attr, - obj.__class__.__name__, - ) - else: - elem = config.etree.SubElement( # type: ignore[attr-defined] - element, - f"{obj.ns}{kml_attr}", # type: ignore[attr-defined] - ) - elem.text = str(attribute) - elif required: - logger.warning( - "Required attribute '%s' for '%s' missing.", - obj_attr, - obj.__class__.__name__, - ) diff --git a/fastkml/kml.py b/fastkml/kml.py index b8b958ef..2d3b02b9 100644 --- a/fastkml/kml.py +++ b/fastkml/kml.py @@ -272,7 +272,7 @@ def author(self, name) -> None: self._atom_author = name elif isinstance(name, str): if self._atom_author is None: - self._atom_author = atom.Author(name=name) + self._atom_author = atom.Author(ns=config.ATOMNS, name=name) else: self._atom_author.name = name elif name is None: @@ -489,14 +489,20 @@ def from_element(self, element: Element, strict: bool = False) -> None: ) atom_link = element.find(f"{atom.NS}link") if atom_link is not None: - s = atom.Link() - s.from_element(atom_link) - self._atom_link = s + self._atom_link = atom.Link.class_from_element( + ns=atom.NS, + name_spaces=self.name_spaces, + element=atom_link, + strict=strict, + ) atom_author = element.find(f"{atom.NS}author") if atom_author is not None: - s = atom.Author() - s.from_element(atom_author) - self._atom_author = s + self._atom_author = atom.Author.class_from_element( + ns=atom.NS, + name_spaces=self.name_spaces, + element=atom_author, + strict=strict, + ) extended_data = element.find(f"{self.ns}ExtendedData") if extended_data is not None: self.extended_data = ExtendedData.class_from_element( diff --git a/fastkml/types.py b/fastkml/types.py index 16288e03..0088ecf2 100644 --- a/fastkml/types.py +++ b/fastkml/types.py @@ -15,14 +15,12 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA """Types for fastkml.""" -from typing import Callable from typing import Iterable from typing import Optional from typing_extensions import Protocol -from typing_extensions import TypedDict -__all__ = ["Element", "KmlObjectMap"] +__all__ = ["Element"] class Element(Protocol): @@ -48,14 +46,3 @@ def append(self, element: "Element") -> None: def remove(self, element: "Element") -> None: """Remove an element from the current element.""" - - -class KmlObjectMap(TypedDict): - """TypedDict for KmlObjectMap.""" - - kml_attr: str - obj_attr: str - required: bool - from_kml: Callable[..., None] - to_kml: Callable[..., None] - validator: Optional[Callable[..., bool]] diff --git a/tests/atom_test.py b/tests/atom_test.py index 533448a1..2b9955b9 100644 --- a/tests/atom_test.py +++ b/tests/atom_test.py @@ -34,6 +34,7 @@ def test_atom_link_ns(self) -> None: def test_atom_link(self) -> None: link = atom.Link( + ns="{http://www.w3.org/2005/Atom}", href="#here", rel="alternate", type="text/html", @@ -53,8 +54,7 @@ def test_atom_link(self) -> None: assert 'length="3456"' in serialized def test_atom_link_read(self) -> None: - link = atom.Link() - link.from_string( + link = atom.Link.class_from_string( '', @@ -67,8 +67,7 @@ def test_atom_link_read(self) -> None: assert link.length == 3456 def test_atom_link_read_no_href(self) -> None: - link = atom.Link() - link.from_string( + link = atom.Link.class_from_string( '', @@ -82,6 +81,7 @@ def test_atom_person_ns(self) -> None: def test_atom_author(self) -> None: a = atom.Author( + ns="{http://www.w3.org/2005/Atom}", name="Nobody", uri="http://localhost", email="cl@donotreply.com", @@ -95,11 +95,11 @@ def test_atom_author(self) -> None: assert "" in serialized def test_atom_author_read(self) -> None: - a = atom.Author() - a.from_string( + a = atom.Author.class_from_string( '' "Nobodyhttp://localhost" "cl@donotreply.com", + ns="{http://www.w3.org/2005/Atom}", ) assert a.name == "Nobody" @@ -107,11 +107,11 @@ def test_atom_author_read(self) -> None: assert a.email == "cl@donotreply.com" def test_atom_contributor_read_no_name(self) -> None: - a = atom.Contributor() - a.from_string( + a = atom.Contributor.class_from_string( '' "http://localhost" "cl@donotreply.com", + ns="{http://www.w3.org/2005/Atom}", ) assert a.name is None @@ -130,8 +130,7 @@ def test_author_roundtrip(self) -> None: a.email = "christian@gmail.com" a.email = "christian" assert "email>" not in str(a.to_string()) - a2 = atom.Author() - a2.from_string(a.to_string()) + a2 = atom.Author.class_from_string(a.to_string()) assert a.to_string() == a2.to_string() def test_link_roundtrip(self) -> None: @@ -140,8 +139,7 @@ def test_link_roundtrip(self) -> None: link.type = "text/html" link.hreflang = "en" link.length = 4096 - l2 = atom.Link() - l2.from_string(link.to_string()) + l2 = atom.Link.class_from_string(link.to_string()) assert link.to_string() == l2.to_string() diff --git a/tests/oldunit_test.py b/tests/oldunit_test.py index 02b8f5fa..d0fca0c0 100644 --- a/tests/oldunit_test.py +++ b/tests/oldunit_test.py @@ -195,6 +195,7 @@ def test_author(self) -> None: d.author = "Christian Ledermann" assert "Christian Ledermann" in str(d.to_string()) a = atom.Author( + ns="{http://www.w3.org/2005/Atom}", name="Nobody", uri="http://localhost", email="cl@donotreply.com", @@ -214,7 +215,7 @@ def test_link(self) -> None: d = kml.Document() d.link = "http://localhost" assert "http://localhost" in str(d.to_string()) - d.link = atom.Link(href="#here") + d.link = atom.Link(ns=config.ATOMNS, href="#here") assert "#here" in str(d.to_string()) # pytest.raises(TypeError, d.link, object) d2 = kml.Document()