From a0b6924c4b5ab6cee4b5b23d8975af70ed27d3dc Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Sat, 14 Dec 2024 14:38:38 +0000 Subject: [PATCH] Add SimpleArrayData class and related helper functions for GX data handling #259 --- fastkml/gx_data.py | 108 +++++++++++++++++++++++++++++++ fastkml/helpers.py | 76 ++++++++++++++++++++++ tests/hypothesis/gx_data_test.py | 56 ++++++++++++++++ 3 files changed, 240 insertions(+) create mode 100644 fastkml/gx_data.py create mode 100644 tests/hypothesis/gx_data_test.py diff --git a/fastkml/gx_data.py b/fastkml/gx_data.py new file mode 100644 index 00000000..7e770978 --- /dev/null +++ b/fastkml/gx_data.py @@ -0,0 +1,108 @@ +# Copyright (C) 2024 Christian Ledermann +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +"""GX SimpleArrayData Extension.""" + +from typing import Dict +from typing import Iterable +from typing import List +from typing import Optional + +from fastkml import config +from fastkml.base import _XMLObject +from fastkml.helpers import attribute_text_kwarg +from fastkml.helpers import clean_string +from fastkml.helpers import subelement_text_list_kwarg +from fastkml.helpers import text_attribute +from fastkml.helpers import text_subelement_list +from fastkml.registry import RegistryItem +from fastkml.registry import registry + +__all__ = ["SimpleArrayData"] + + +class SimpleArrayData(_XMLObject): + """ + A SimpleArrayData element. + + This element is used to define an array of string values. It is used in + conjunction with the gx:SimpleArrayField element to specify how the array + values are to be displayed. + """ + + _default_nsid = config.GX + name: Optional[str] + data: List[str] + + def __init__( + self, + ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, + name: Optional[str] = None, + data: Optional[Iterable[str]] = None, + ) -> None: + """ + Create a SimpleArrayData element. + + Args: + ns: The namespace to use. + name_spaces: A dictionary of namespace prefixes to namespace URIs. + name: The name of the element. + data: A list of string values. + + """ + super().__init__(ns=ns, name_spaces=name_spaces) + self.data = list(data) if data is not None else [] + self.name = clean_string(name) + + def __repr__(self) -> str: + """Create a string (c)representation for SimpleArrayData.""" + return ( + f"{self.__class__.__module__}.{self.__class__.__name__}(" + f"ns={self.ns!r}, " + f"name_spaces={self.name_spaces!r}, " + f"name={self.name!r}, " + f"data={self.data!r}, " + ")" + ) + + def __bool__(self) -> bool: + """Check if the element is named and has any data.""" + return bool(self.data) and bool(self.name) + + +registry.register( + SimpleArrayData, + RegistryItem( + ns_ids=("gx", ""), + classes=(str,), + attr_name="data", + node_name="value", + get_kwarg=subelement_text_list_kwarg, + set_element=text_subelement_list, + ), +) +registry.register( + SimpleArrayData, + RegistryItem( + ns_ids=("gx", ""), + classes=(str,), + attr_name="name", + node_name="name", + get_kwarg=attribute_text_kwarg, + set_element=text_attribute, + ), +) diff --git a/fastkml/helpers.py b/fastkml/helpers.py index 22010293..f587da44 100644 --- a/fastkml/helpers.py +++ b/fastkml/helpers.py @@ -269,6 +269,48 @@ def text_subelement( subelement.text = value +def text_subelement_list( + obj: "_XMLObject", + *, + element: Element, + attr_name: str, + node_name: str, + precision: Optional[int], + verbosity: Verbosity, + default: Optional[str], +) -> None: + """ + Set the value of an attribute from subelements with a text node. + + Args: + ---- + obj ("_XMLObject"): The object from which to retrieve the attribute value. + element (Element): The parent element to add the subelement to. + attr_name (str): The name of the attribute to retrieve the value from. + node_name (str): The name of the subelement to create. + precision (Optional[int]): The precision of the attribute value. + verbosity (Optional[Verbosity]): The verbosity level. + default (Optional[str]): The default value for the attribute. + + Returns: + ------- + None + + """ + if value := get_value( + obj, + attr_name=attr_name, + verbosity=verbosity, + default=default, + ): + for item in value: + subelement = config.etree.SubElement( + element, + f"{obj.ns}{node_name}", + ) + subelement.text = item + + def text_attribute( obj: "_XMLObject", *, @@ -710,6 +752,40 @@ def subelement_text_kwarg( return {kwarg: node.text.strip()} if node.text and node.text.strip() else {} +def subelement_text_list_kwarg( + *, + element: Element, + ns: str, + name_spaces: Dict[str, str], + node_name: str, + kwarg: str, + classes: Tuple[Type[object], ...], + strict: bool, +) -> Dict[str, List[str]]: + """ + Extract the text content of subelements and return it as a dictionary. + + Args: + ---- + element (Element): The parent element. + ns (str): The namespace of the subelement. + name_spaces (Dict[str, str]): A dictionary of namespace prefixes and URIs. + node_name (str): The name of the subelement. + kwarg (str): The key to use in the returned dictionary. + classes (Tuple[Type[object], ...]): A tuple of known types. + strict (bool): A flag indicating whether to enforce strict parsing. + + Returns: + ------- + Dict[str, List[str]]: A dictionary containing the extracted text contents, + with the specified key. + + """ + if nodes := element.findall(f"{ns}{node_name}"): + return {kwarg: [node.text.strip() for node in nodes if node.text.strip()]} + return {} + + def attribute_text_kwarg( *, element: Element, diff --git a/tests/hypothesis/gx_data_test.py b/tests/hypothesis/gx_data_test.py new file mode 100644 index 00000000..c2be0e1c --- /dev/null +++ b/tests/hypothesis/gx_data_test.py @@ -0,0 +1,56 @@ +# Copyright (C) 2024 Christian Ledermann +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +"""Test gx SimpleArrayData.""" + +import typing + +from hypothesis import given +from hypothesis import strategies as st + +import fastkml +import fastkml.gx_data +import fastkml.types +from tests.base import Lxml +from tests.hypothesis.common import assert_repr_roundtrip +from tests.hypothesis.common import assert_str_roundtrip +from tests.hypothesis.common import assert_str_roundtrip_terse +from tests.hypothesis.common import assert_str_roundtrip_verbose +from tests.hypothesis.strategies import nc_name +from tests.hypothesis.strategies import xml_text + + +class TestGx(Lxml): + @given( + name=st.one_of(st.none(), nc_name()), + data=st.one_of( + st.none(), + st.lists(xml_text().filter(lambda x: x.strip() != "")), + ), + ) + def test_fuzz_simple_array_data( + self, + name: typing.Optional[str], + data: typing.Optional[typing.Iterable[str]], + ) -> None: + simple_array_data = fastkml.gx_data.SimpleArrayData( + name=name, + data=data, + ) + + assert_repr_roundtrip(simple_array_data) + assert_str_roundtrip(simple_array_data) + assert_str_roundtrip_terse(simple_array_data) + assert_str_roundtrip_verbose(simple_array_data)