# Copyright (C) 2012 - 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
"""
Abstract XML base class.
The purpose of ``_XMLObject`` is to serve as a base class for KML objects in fastkml.
Its main functions are:
- Provide a common interface for XML serialization and deserialization.
- Handle namespace management for KML elements.
- Manage attribute storage and retrieval for derived classes.
- Provide the ``etree_element()`` method for converting objects to XML Elements.
- Facilitate integration with the registry system for flexible XML mapping.
By inheriting from ``_XMLObject``, KML classes gain these capabilities, ensuring
consistent handling of XML operations across the library.
"""
import logging
from typing import Any
from typing import Dict
from typing import Optional
from typing import Tuple
from typing import cast
from typing_extensions import Self
from fastkml import config
from fastkml.enums import Verbosity
from fastkml.registry import registry
from fastkml.types import Element
from fastkml.validator import validate
logger = logging.getLogger(__name__)
__all__ = ["_XMLObject"]
[docs]
class _XMLObject:
"""XML Baseclass."""
_default_nsid: str = ""
_node_name: str = ""
name_spaces: Dict[str, str]
__kwarg_keys: Tuple[str, ...]
[docs]
def __init__(
self,
ns: Optional[str] = None,
name_spaces: Optional[Dict[str, str]] = None,
**kwargs: Any,
) -> None:
"""
Initialize the XML Object.
Parameters
----------
ns : Optional[str], default=None
The namespace of the XML object.
name_spaces : Optional[Dict[str, str]], default=None
The dictionary of namespace prefixes and URIs.
**kwargs : Any
Additional keyword arguments.
"""
name_spaces = name_spaces or {}
self.name_spaces = {**config.NAME_SPACES, **name_spaces}
self.ns: str = (
self.name_spaces.get(self._default_nsid, "") if ns is None else ns
)
for arg, val in kwargs.items():
setattr(self, arg, val)
self.__kwarg_keys = tuple(kwargs.keys())
def __repr__(self) -> str:
"""
Create a string (c)representation for _XMLObject.
Returns
-------
str
The string representation of the object.
"""
return (
f"{self.__class__.__module__}.{self.__class__.__name__}("
f"ns={self.ns!r}, "
f"name_spaces={self.name_spaces!r}, "
f"**{self._get_splat()!r},"
")"
)
def __str__(self) -> str:
"""
Return the string representation of the object.
Returns
-------
str
The string representation of the object.
"""
return self.to_string()
def __eq__(self, other: object) -> bool:
"""
Compare two _XMLObject instances for equality.
Parameters
----------
other : object
The object to compare with.
Returns
-------
bool
True if the objects are equal, False otherwise.
"""
return self.__dict__ == other.__dict__ if type(self) is type(other) else False
[docs]
def etree_element(
self,
precision: Optional[int] = None,
verbosity: Verbosity = Verbosity.normal,
) -> Element:
"""
Return the KML Object as an Element.
This method essentially converts the Python object to its XML representation,
using the registry to determine how each attribute should be serialized.
- Create an XML Element with the object's tag name and namespace.
- Iterate through registered attributes for the object's class.
For each attribute:
- Call the corresponding set_element function. This function adds the
attribute to the Element as a sub-element or attribute.
- Handle different data types and nested objects.
- Apply precision and verbosity settings if specified.
- Return the complete Element tree representing the object.
Parameters
----------
precision : Optional[int], default=None
The precision of the KML object.
verbosity : Verbosity, default=Verbosity.normal
The verbosity level.
Returns
-------
Element
The KML object as an Element.
"""
element: Element = config.etree.Element(
f"{self.ns}{self.get_tag_name()}",
)
for item in registry.get(self.__class__):
item.set_element(
obj=self,
element=element,
attr_name=item.attr_name,
node_name=item.node_name,
precision=precision,
verbosity=verbosity,
default=item.default,
)
return element
[docs]
def to_string(
self,
*,
prettyprint: bool = True,
precision: Optional[int] = None,
verbosity: Verbosity = Verbosity.normal,
) -> str:
"""
Return the KML Object as serialized xml.
Parameters
----------
prettyprint : bool, default=True
Whether to pretty print the XML.
precision : Optional[int], default=None
The precision of the KML object.
verbosity : Verbosity, default=Verbosity.normal
The verbosity level.
Returns
-------
str
The KML object as serialized XML.
"""
element = self.etree_element(
precision=precision,
verbosity=verbosity,
)
try:
return cast(
"str",
config.etree.tostring(
element,
encoding="unicode",
pretty_print=prettyprint,
),
)
except TypeError:
return cast(
"str",
config.etree.tostring(
element,
encoding="unicode",
),
)
[docs]
def validate(self) -> Optional[bool]:
"""
Validate the KML object against the XML schema.
Returns
-------
Optional[bool]
True if the object is valid, None if the XMLSchema is not available.
Raises
------
AssertionError
If the object is not valid.
"""
return validate(element=self.etree_element())
def _get_splat(self) -> Dict[str, Any]:
"""
Get the keyword arguments as a dictionary.
Returns
-------
Dict[str, Any]
The keyword arguments as a dictionary.
"""
return {
key: getattr(self, key)
for key in self.__kwarg_keys
if getattr(self, key) is not None
}
@classmethod
def get_tag_name(cls) -> str:
"""
Return the tag name.
Returns
-------
str
The tag name.
"""
return cls.__name__
@classmethod
def _get_ns(cls, ns: Optional[str], name_spaces: Dict[str, str]) -> str:
"""
Get the namespace.
Parameters
----------
ns : Optional[str]
The namespace.
name_spaces : Dict[str, str]
The dictionary of namespace prefixes and URIs.
Returns
-------
str
The namespace.
"""
return name_spaces.get(cls._default_nsid, "") if ns is None else ns
[docs]
@classmethod
def _get_kwargs(
cls,
*,
ns: str,
name_spaces: Optional[Dict[str, str]] = None,
element: Element,
strict: bool,
) -> Dict[str, Any]:
"""
Get the keyword arguments for the class constructor.
A class method used for XML deserialization. Its main purposes are:
- Extract attribute values from an XML element.
- Convert these values into appropriate Python types.
- Prepare a dictionary of keyword arguments for object initialization.
- It is called during the parsing process to populate object attributes from XML
data. The method uses the registry and helper functions to handle different
attribute types and nested objects.
Subclasses may override this method to add custom deserialization logic for
specific KML elements, although this should be rare. Prefer registration over
a custom ``_get_kwargs`` implementation to ensure consistent handling of KML:
- Consistency: Ensures uniform handling across all KML elements.
- Maintainability: Centralizes parsing logic, making updates easier.
- Declarative approach: Simplifies code by moving logic to configuration.
- Reusability: Allows sharing of parsing logic across different classes.
- Separation of concerns: Keeps parsing logic separate from class definitions.
- Extensibility: Makes it easier to add new attributes or change parsing
behavior.
- Reduced duplication: Avoids repeating similar parsing code in multiple
classes.
- Easier testing: Allows testing of parsing logic independently of class
implementations.
Parameters
----------
ns : str
The namespace.
name_spaces : Optional[Dict[str, str]], default=None
The dictionary of namespace prefixes and URIs.
element : Element
The XML element.
strict : bool
Whether to enforce strict parsing.
Returns
-------
Dict[str, Any]
The keyword arguments 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}
for item in registry.get(cls):
for name_space in item.ns_ids:
kwarg = item.get_kwarg(
element=element,
ns=name_spaces.get(name_space, ""),
name_spaces=name_spaces,
node_name=item.node_name,
kwarg=item.attr_name,
classes=item.classes,
strict=strict,
)
if kwarg:
kwargs.update(
kwarg,
)
break
return kwargs
@classmethod
def class_from_element(
cls,
*,
ns: str,
name_spaces: Optional[Dict[str, str]] = None,
element: Element,
strict: bool,
) -> Self:
"""
Create an XML object from an etree element.
Parameters
----------
ns : str
The namespace.
name_spaces : Optional[Dict[str, str]], default=None
The dictionary of namespace prefixes and URIs.
element : Element
The XML element.
strict : bool
Whether to enforce strict parsing.
Returns
-------
Self
The XML object.
"""
kwargs = cls._get_kwargs(
ns=ns,
name_spaces=name_spaces,
element=element,
strict=strict,
)
return cls(
**kwargs,
)
[docs]
@classmethod
def from_string(
cls,
string: str,
*,
ns: Optional[str] = None,
name_spaces: Optional[Dict[str, str]] = None,
strict: bool = True,
) -> Self:
"""
Create an XML object from a string.
Parameters
----------
string : str
The string representation of the XML object.
ns : Optional[str], default=None
The namespace of the XML object.
name_spaces : Optional[Dict[str, str]], default=None
The dictionary of namespace prefixes and URIs.
strict : bool, default=True
Whether to enforce strict parsing.
Returns
-------
Self
The XML object.
"""
name_spaces = name_spaces or {}
name_spaces = {**config.NAME_SPACES, **name_spaces}
ns = cls._get_ns(ns, name_spaces=name_spaces)
return cls.class_from_element(
ns=ns,
name_spaces=name_spaces,
strict=strict,
element=cast(
"Element",
config.etree.fromstring(string),
),
)