From 2aad8b1e82ef5f1c489e1c85c21f0c132bb0efa3 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Thu, 20 Jun 2024 12:43:45 +0100 Subject: [PATCH 01/12] document the helper functions --- fastkml/helpers.py | 357 +++++++++++++++++++++++++++++++++++++++++---- pyproject.toml | 4 + 2 files changed, 335 insertions(+), 26 deletions(-) diff --git a/fastkml/helpers.py b/fastkml/helpers.py index 48eb19f7..193365ec 100644 --- a/fastkml/helpers.py +++ b/fastkml/helpers.py @@ -1,4 +1,4 @@ -# Copyright (C) 2023 Christian Ledermann +# Copyright (C) 2023 - 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 @@ -14,6 +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 """Helper functions for fastkml.""" + import logging from enum import Enum from typing import Dict @@ -75,8 +76,7 @@ def handle_error( ) if strict: raise KMLParseError(msg) from error - else: - logger.warning("%s, %s", error, msg) + logger.warning("%s, %s", error, msg) def node_text( @@ -88,6 +88,30 @@ def node_text( precision: Optional[int], verbosity: Optional[Verbosity], ) -> None: + """ + Set the text of an XML element based on the attribute value in the given object. + + Parameters + ---------- + obj : _XMLObject + The object containing the attribute value. + element : Element + The XML element to set the text content for. + attr_name : str + The name of the attribute in the object. + node_name : str + The name of the XML node (unused). + precision : Optional[int] + The precision to use when converting numeric values to text (unused). + verbosity : Optional[Verbosity] + The verbosity level for logging (unused). + + Returns + ------- + None + This function does not return anything. + + """ if getattr(obj, attr_name, None): element.text = getattr(obj, attr_name) @@ -327,6 +351,23 @@ def xml_subelement( precision: Optional[int], verbosity: Optional[Verbosity], ) -> None: + """ + Add a subelement to an XML element based on the value of an attribute of an object. + + Args: + ---- + obj (_XMLObject): The object containing the attribute. + element (Element): The XML element to which the subelement will be added. + attr_name (str): The name of the attribute in the object. + node_name (str): The name of the XML node for the subelement (unused). + precision (Optional[int]): The precision for formatting numerical values. + verbosity (Optional[Verbosity]): The verbosity level for the subelement. + + Returns: + ------- + None + + """ if getattr(obj, attr_name, None): element.append( getattr(obj, attr_name).etree_element( @@ -345,6 +386,23 @@ def xml_subelement_list( precision: Optional[int], verbosity: Optional[Verbosity], ) -> None: + """ + Add subelements to an XML element based on a list attribute of an object. + + Args: + ---- + obj (_XMLObject): The object containing the list attribute. + element (Element): The XML element to which the subelements will be added. + attr_name (str): The name of the list attribute in the object. + node_name (str): The name of the XML node for each subelement (unused). + precision (Optional[int]): The precision for floating-point values. + verbosity (Optional[Verbosity]): The verbosity level for the XML output. + + Returns: + ------- + None + + """ if getattr(obj, attr_name, None): for item in getattr(obj, attr_name): if item: @@ -363,6 +421,26 @@ def node_text_kwarg( classes: Tuple[known_types, ...], strict: bool, ) -> Dict[str, str]: + """ + Extract the text content of an XML element and return it as a dictionary. + + Args: + ---- + element (Element): The XML element to extract the text content from. + ns (str): The namespace of the XML element. + name_spaces (Dict[str, str]): + A dictionary mapping namespace prefixes to their URIs. + node_name (str): The name of the XML node. + kwarg (str): The name of the keyword argument to store the text content in. + classes (Tuple[known_types, ...]): A tuple of known types. + strict (bool): A flag indicating whether to enforce strict parsing rules. + + Returns: + ------- + Dict[str, str]: A dictionary containing the keyword argument and its + corresponding text content, if it exists. + + """ return ( {kwarg: element.text.strip()} if element.text and element.text.strip() else {} ) @@ -378,6 +456,25 @@ def subelement_text_kwarg( classes: Tuple[known_types, ...], strict: bool, ) -> Dict[str, str]: + """ + Extract the text content of a subelement 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[known_types, ...]): A tuple of known types. + strict (bool): A flag indicating whether to enforce strict parsing. + + Returns: + ------- + Dict[str, str]: A dictionary containing the extracted text content, + with the specified key. + + """ node = element.find(f"{ns}{node_name}") if node is None: return {} @@ -394,11 +491,29 @@ def attribute_text_kwarg( classes: Tuple[known_types, ...], strict: bool, ) -> Dict[str, str]: + """ + Return a dictionary representing the attribute as a keyword argument. + + Args: + ---- + element (Element): The XML element. + ns (str): The namespace of the attribute. + name_spaces (Dict[str, str]): A dictionary mapping namespace prefixes to URIs. + node_name (str): The name of the XML node. + kwarg (str): The name of the keyword argument. + classes (Tuple[known_types, ...]): A tuple of known types. + strict (bool): A flag indicating whether to enforce strict parsing. + + Returns: + ------- + Dict[str, str]: A dictionary representing the attribute as a keyword argument. + + """ attr = element.get(f"{ns}{node_name}") return {kwarg: attr} if attr else {} -def _get_boolean_value(text: str, strict: bool) -> bool: +def _get_boolean_value(*, text: str, strict: bool) -> bool: if not strict: text = text.lower() if text in {"1", "true"}: @@ -407,7 +522,8 @@ def _get_boolean_value(text: str, strict: bool) -> bool: return False if not strict: return bool(float(text)) - raise ValueError(f"Invalid boolean value: {text}") + msg = f"Value {text} is not a valid value for Boolean" + raise ValueError(msg) def subelement_bool_kwarg( @@ -420,14 +536,36 @@ def subelement_bool_kwarg( classes: Tuple[known_types, ...], strict: bool, ) -> Dict[str, bool]: - assert len(classes) == 1 - assert issubclass(classes[0], bool) + """ + Extract a boolean value from a subelement of an XML element. + + Args: + ---- + element (Element): The XML element to search for the subelement. + ns (str): The namespace of the subelement. + name_spaces (Dict[str, str]): A dictionary mapping namespace prefixes to URIs. + node_name (str): The name of the subelement. + kwarg (str): The name of the keyword argument to store the boolean value. + classes (Tuple[known_types, ...]): A tuple of known types. + strict (bool): A flag indicating whether to enforce strict parsing. + + Returns: + ------- + Dict[str, bool]: A dictionary containing the keyword argument and its value. + + Raises: + ------ + ValueError: If the value of the subelement is not a valid boolean. + + """ + assert len(classes) == 1 # noqa: S101 + assert issubclass(classes[0], bool) # noqa: S101 node = element.find(f"{ns}{node_name}") if node is None: return {} if node.text and node.text.strip(): try: - return {kwarg: _get_boolean_value(node.text.strip(), strict)} + return {kwarg: _get_boolean_value(text=node.text.strip(), strict=strict)} except ValueError as exc: handle_error( error=exc, @@ -449,6 +587,28 @@ def subelement_int_kwarg( classes: Tuple[known_types, ...], strict: bool, ) -> Dict[str, int]: + """ + Extract an integer value from a subelement of an XML element. + + Args: + ---- + element (Element): The XML element to search for the subelement. + ns (str): The namespace of the subelement. + name_spaces (Dict[str, str]): A dictionary mapping namespace prefixes to URIs. + node_name (str): The name of the subelement. + kwarg (str): The key to use in the returned dictionary. + classes (Tuple[known_types, ...]): A tuple of known types for error handling. + strict (bool): A flag indicating whether to enforce strict parsing. + + Returns: + ------- + Dict[str, int]: A dictionary containing the keyword argument and its value. + + Raises: + ------ + ValueError: If the value of the subelement is not a valid integer and strict. + + """ node = element.find(f"{ns}{node_name}") if node is None: return {} @@ -476,6 +636,24 @@ def attribute_int_kwarg( classes: Tuple[known_types, ...], strict: bool, ) -> Dict[str, int]: + """ + Extract an integer attribute from an XML element and return it as a dictionary. + + Args: + ---- + element (Element): The XML element to extract the attribute from. + ns (str): The namespace of the attribute. + name_spaces (Dict[str, str]): A dictionary mapping namespace prefixes to URIs. + node_name (str): The name of the XML node containing the attribute. + kwarg (str): The name of the keyword argument to store the extracted attribute. + classes (Tuple[known_types, ...]): A tuple of known types (unused). + strict (bool): A flag indicating whether to raise an exception (unused). + + Returns: + ------- + Dict[str, int]: A dictionary containing the extracted attribute value. + + """ attr = element.get(f"{ns}{node_name}") return {kwarg: int(attr)} if attr else {} @@ -490,6 +668,28 @@ def subelement_float_kwarg( classes: Tuple[known_types, ...], strict: bool, ) -> Dict[str, float]: + """ + Extract a float value from a subelement of an XML element. + + Args: + ---- + element (Element): The XML element to search for the subelement. + 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 name of the keyword argument to store the float value. + classes (Tuple[known_types, ...]): A tuple of known types for error handling. + strict (bool): A flag indicating whether to raise an error. + + Returns: + ------- + Dict[str, float]: A dictionary containing the float value as a keyword argument. + + Raises: + ------ + ValueError: If the value of the subelement cannot be converted and strict. + + """ node = element.find(f"{ns}{node_name}") if node is None: return {} @@ -517,6 +717,28 @@ def attribute_float_kwarg( classes: Tuple[known_types, ...], strict: bool, ) -> Dict[str, float]: + """ + Convert an attribute value to a float and return it as a dictionary. + + Args: + ---- + element (Element): The XML element containing the attribute. + ns (str): The namespace of the attribute. + name_spaces (Dict[str, str]): A dictionary of namespace prefixes and URIs. + node_name (str): The name of the attribute. + kwarg (str): The name of the keyword argument to store the converted float. + classes (Tuple[known_types, ...]): A tuple of known types for error handling. + strict (bool): A flag indicating whether to raise an error for invalid values. + + Returns: + ------- + Dict[str, float]: A dictionary containing the float as a keyword argument. + + Raises: + ------ + ValueError: If the attribute value cannot be converted to a float. + + """ attr = element.get(f"{ns}{node_name}") try: return {kwarg: float(attr)} if attr else {} @@ -531,7 +753,7 @@ def attribute_float_kwarg( return {} -def _get_enum_value(enum_class: Type[Enum], text: str, strict: bool) -> Enum: +def _get_enum_value(*, enum_class: Type[Enum], text: str, strict: bool) -> Enum: value = enum_class(text) if strict and value.value != text: msg = f"Value {text} is not a valid value for Enum {enum_class.__name__}" @@ -549,15 +771,43 @@ def subelement_enum_kwarg( classes: Tuple[known_types, ...], strict: bool, ) -> Dict[str, Enum]: - assert len(classes) == 1 - assert issubclass(classes[0], Enum) + """ + Extract an enumerated value from a subelement of an XML element. + + Args: + ---- + element (Element): The XML element to search for the subelement. + 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 name of the keyword argument to store the extracted value. + classes (Tuple[known_types, ...]): A tuple of enumerated value classes. + strict (bool): A flag indicating whether to raise an exception. + + Returns: + ------- + Dict[str, Enum]: A dictionary containing the extracted enumerated value. + + Raises: + ------ + ValueError: If the extracted value is not a valid enumerated value and strict. + + """ + assert len(classes) == 1 # noqa: S101 + assert issubclass(classes[0], Enum) # noqa: S101 node = element.find(f"{ns}{node_name}") if node is None: return {} node_text = node.text.strip() if node.text else "" if node_text: try: - return {kwarg: _get_enum_value(classes[0], node_text, strict)} + return { + kwarg: _get_enum_value( + enum_class=classes[0], + text=node_text, + strict=strict, + ), + } except ValueError as exc: handle_error( error=exc, @@ -579,18 +829,35 @@ def attribute_enum_kwarg( classes: Tuple[known_types, ...], strict: bool, ) -> Dict[str, Enum]: - assert len(classes) == 1 - assert issubclass(classes[0], Enum) + """ + Return a dictionary with the specified keyword argument and its enum value. + + Args: + ---- + element (Element): The XML element. + ns (str): The namespace of the XML element. + name_spaces (Dict[str, str]): A dictionary of namespace prefixes and their URIs. + node_name (str): The name of the XML node. + kwarg (str): The name of the keyword argument. + classes (Tuple[known_types, ...]): A tuple of enum classes. + strict (bool): A flag indicating whether to raise an error for invalid values. + + Returns: + ------- + Dict[str, Enum]: A dictionary with the specified keyword argument and its value. + + """ + assert len(classes) == 1 # noqa: S101 + assert issubclass(classes[0], Enum) # noqa: S101 if raw := element.get(f"{ns}{node_name}"): try: - value = classes[0](raw) - if raw != value.value and strict: - msg = ( - f"Value {raw} is not a valid value for Enum " - f"{classes[0].__name__}" - ) - raise ValueError(msg) - return {kwarg: classes[0](raw)} + return { + kwarg: _get_enum_value( + enum_class=classes[0], + text=raw, + strict=strict, + ), + } except ValueError as exc: handle_error( error=exc, @@ -612,8 +879,27 @@ def xml_subelement_kwarg( classes: Tuple[known_types, ...], strict: bool, ) -> Dict[str, _XMLObject]: + """ + Return the subelement of the given XML element based on the provided parameters. + + Args: + ---- + element (Element): The XML element to search within. + ns (str): The namespace of the XML element. + name_spaces (Dict[str, str]): A dictionary mapping namespace prefixes to their URIs. + node_name (str): The name of the XML node to search for. + kwarg (str): The name of the keyword argument to store the found subelement. + classes (Tuple[known_types, ...]): A tuple of classes that represent the types. + strict (bool): A flag indicating whether to enforce strict parsing rules. + + Returns: + ------- + Dict[str, _XMLObject]: A dictionary containing the found subelement as the value + of the specified keyword argument. + + """ for cls in classes: - assert issubclass(cls, _XMLObject) + assert issubclass(cls, _XMLObject) # noqa: S101 subelement = element.find(f"{ns}{cls.get_tag_name()}") if subelement is not None: return { @@ -637,11 +923,30 @@ def xml_subelement_list_kwarg( classes: Tuple[known_types, ...], strict: bool, ) -> Dict[str, List[_XMLObject]]: + """ + Return a dictionary with the specified keyword argument and its list of subelements. + + Args: + ---- + element (Element): The XML element to search within. + ns (str): The namespace of the XML element. + name_spaces (Dict[str, str]): A dictionary mapping namespace prefixes to URIs. + node_name (str): The name of the XML node to search for. + kwarg (str): The name of the keyword argument to store the found subelements. + classes (Tuple[known_types, ...]): A tuple of classes that represent the types. + strict (bool): A flag indicating whether to enforce strict parsing rules. + + Returns: + ------- + Dict[str, List[_XMLObject]]: A dictionary containing the specified keyword + argument and its list of subelements. + + """ args_list = [] - assert node_name is not None - assert name_spaces is not None + assert node_name is not None # noqa: S101 + assert name_spaces is not None # noqa: S101 for obj_class in classes: - assert issubclass(obj_class, _XMLObject) + assert issubclass(obj_class, _XMLObject) # noqa: S101 if subelements := element.findall(f"{ns}{obj_class.get_tag_name()}"): args_list.extend( [ diff --git a/pyproject.toml b/pyproject.toml index a0af9304..cb237d33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -245,6 +245,10 @@ select = [ ] [tool.ruff.lint.extend-per-file-ignores] +"fastkml/helpers.py" = [ + "ARG001", + "PLR0913", +] "tests/*.py" = [ "D101", "D102", From 5b7ee34fdd97d5952e56dcbaf9d9cd821dc95cab Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Thu, 18 Jul 2024 15:59:16 +0100 Subject: [PATCH 02/12] better docstrings and ruff fixes --- fastkml/atom.py | 159 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 123 insertions(+), 36 deletions(-) diff --git a/fastkml/atom.py b/fastkml/atom.py index d841c23c..3e02bd5d 100644 --- a/fastkml/atom.py +++ b/fastkml/atom.py @@ -15,6 +15,8 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA """ +Basic Atom support for KML. + KML 2.2 supports new elements for including data about the author and related website in your KML file. This information is displayed in geo search results, both in Earth browsers such as Google Earth, and in other applications such as @@ -72,7 +74,9 @@ def get_tag_name(cls) -> str: class Link(_AtomObject): """ - Identifies a related Web page. The rel attribute defines the type of relation. + 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, @@ -80,44 +84,55 @@ class Link(_AtomObject): """ href: Optional[str] - # href is the URI of the referenced resource - 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): - # alternate: an alternate representation - # enclosure: a related resource which is potentially large in size - # and might require special handling, for example an audio or video - # recording. - # related: an document related to the entry or feed. - # self: the feed itself. - # via: the source of the information provided in the entry. - type: Optional[str] - # indicates the media type of the resource - hreflang: Optional[str] - # indicates the language of the referenced resource - title: Optional[str] - # human readable information about the link - length: Optional[int] - # the length of the resource, in bytes - def __init__( + def __init__( # noqa: PLR0913 self, ns: Optional[str] = None, name_spaces: Optional[Dict[str, str]] = None, href: Optional[str] = None, rel: Optional[str] = None, - type: Optional[str] = None, + type: Optional[str] = None, # noqa: A002 hreflang: Optional[str] = None, title: Optional[str] = None, length: Optional[int] = None, **kwargs: Any, ) -> None: + """ + Initialize a Link object. + + Parameters + ---------- + ns : str, optional + The namespace of the Link object. + name_spaces : dict, optional + The dictionary of namespace prefixes and URIs. + href : str, optional + The URI of the referenced resource. + rel : str, optional + The link relationship type. It can be a full URI or one of the + following predefined values: 'alternate', 'enclosure', 'related', + 'self', or 'via'. + type : str, optional + The media type of the resource. + hreflang : str, optional + The language of the referenced resource. + title : str, optional + Human-readable information about the link. + length : int, optional + The length of the resource in bytes. + kwargs : dict, optional + Additional keyword arguments. + + Returns + ------- + None + + """ super().__init__(ns=ns, name_spaces=name_spaces, **kwargs) self.href = href self.rel = rel @@ -127,7 +142,15 @@ def __init__( self.length = length def __repr__(self) -> str: - """Create a string (c)representation for Link.""" + """ + Return a string representation of the Link object. + + Returns + ------- + str + The string representation of the Link object. + + """ return ( f"{self.__class__.__module__}.{self.__class__.__name__}(" f"ns={self.ns!r}, " @@ -143,11 +166,34 @@ def __repr__(self) -> str: ) def __bool__(self) -> bool: + """ + Check if the Link object is truthy. + + Returns + ------- + bool + True if the Link object has a href attribute, False otherwise. + + """ return bool(self.href) def __eq__(self, other: object) -> bool: + """ + Check if the Link object is equal to another object. + + Parameters + ---------- + other : object + The object to compare with. + + Returns + ------- + bool + True if the Link object is equal to the other object, False otherwise. + + """ try: - assert isinstance(other, type(self)) + assert isinstance(other, type(self)) # noqa: S101 except AssertionError: return False return ( @@ -232,21 +278,21 @@ def __eq__(self, other: object) -> bool: class _Person(_AtomObject): """ - and describe a person, corporation, or similar - entity. It has one required element, name, and two optional elements: - uri, email. + Represents a person, corporation, or similar entity. + + Attributes + ---------- + name (Optional[str]): A human-readable name for the person. + uri (Optional[str]): A home page for the person. + email (Optional[str]): An email address for the person. + """ name: Optional[str] - # conveys a human-readable name for the person. - uri: Optional[str] - # contains a home page for the person. - email: Optional[str] - # contains an email address for the person. - def __init__( + def __init__( # noqa: PLR0913 self, ns: Optional[str] = None, name_spaces: Optional[Dict[str, str]] = None, @@ -255,13 +301,33 @@ def __init__( email: Optional[str] = None, **kwargs: Any, ) -> None: + """ + Initialize a new instance of the _Person class. + + Args: + ---- + ns (Optional[str]): The namespace for the XML element. + name_spaces (Optional[Dict[str, str]]): The namespace dictionary. + name (Optional[str]): A human-readable name for the person. + uri (Optional[str]): A home page for the person. + email (Optional[str]): An email address for the person. + **kwargs: Additional keyword arguments. + + """ super().__init__(ns=ns, name_spaces=name_spaces, **kwargs) self.name = name self.uri = uri self.email = email def __repr__(self) -> str: - """Create a string (c)representation for _Person.""" + """ + Return a string representation of the _Person object. + + Returns + ------- + str: The string representation of the _Person object. + + """ return ( f"{self.__class__.__module__}.{self.__class__.__name__}(" f"ns={self.ns!r}, " @@ -274,11 +340,32 @@ def __repr__(self) -> str: ) def __bool__(self) -> bool: + """ + Check if the _Person object has a name. + + Returns + ------- + bool: True if the _Person object has a name, False otherwise. + + """ return bool(self.name) def __eq__(self, other: object) -> bool: + """ + Check if the _Person object is equal to another object. + + Args: + ---- + other (object): The object to compare with. + + Returns: + ------- + bool: True if the _Person object is equal to the other object, + False otherwise. + + """ try: - assert isinstance(other, type(self)) + assert isinstance(other, type(self)) # noqa: S101 except AssertionError: return False return ( From 7fee7b0c7eb6ce2d9fdad7ca510378a7856b91a9 Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Thu, 18 Jul 2024 16:18:12 +0100 Subject: [PATCH 03/12] better docstrings and ruff fixes --- fastkml/containers.py | 74 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 62 insertions(+), 12 deletions(-) diff --git a/fastkml/containers.py b/fastkml/containers.py index e513701b..12ed79ef 100644 --- a/fastkml/containers.py +++ b/fastkml/containers.py @@ -63,9 +63,9 @@ class _Container(_Feature): """ - abstract element; do not create - A Container element holds one or more Features and allows the - creation of nested hierarchies. + A Container element holds one or more Features. + + It allows the creation of nested hierarchies. subclasses are: Document, Folder. @@ -73,11 +73,11 @@ class _Container(_Feature): features: List[_Feature] - def __init__( + def __init__( # noqa: PLR0913 self, ns: Optional[str] = None, name_spaces: Optional[Dict[str, str]] = None, - id: Optional[str] = None, + id: Optional[str] = None, # noqa: A002 target_id: Optional[str] = None, name: Optional[str] = None, visibility: Optional[bool] = None, @@ -155,31 +155,33 @@ def append(self, kmlobj: _Feature) -> None: if kmlobj is self: msg = "Cannot append self" raise ValueError(msg) - assert self.features is not None + assert self.features is not None # noqa: S101 self.features.append(kmlobj) class Folder(_Container): """ - A Folder is used to arrange other Features hierarchically - (Folders, Placemarks, #NetworkLinks, or #Overlays). + A Folder is used to arrange other Features hierarchically. + + It may contain Folders, Placemarks, NetworkLinks, or Overlays. """ 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 + 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. """ schemata: List[Schema] - def __init__( + def __init__( # noqa: PLR0913 self, ns: Optional[str] = None, name_spaces: Optional[Dict[str, str]] = None, - id: Optional[str] = None, + id: Optional[str] = None, # noqa: A002 target_id: Optional[str] = None, name: Optional[str] = None, visibility: Optional[bool] = None, @@ -200,6 +202,40 @@ def __init__( schemata: Optional[Iterable[Schema]] = None, **kwargs: Any, ) -> None: + """ + Initialize a new instance of the class. + + Args: + ---- + ns (Optional[str]): The namespace. + name_spaces (Optional[Dict[str, str]]): + The dictionary of namespace prefixes and URIs. + id (Optional[str]): The ID of the container. + target_id (Optional[str]): The target ID. + name (Optional[str]): The name of the container. + visibility (Optional[bool]): The visibility flag. + isopen (Optional[bool]): The isopen flag. + atom_link (Optional[atom.Link]): The Atom link. + atom_author (Optional[atom.Author]): The Atom author. + address (Optional[str]): The address. + phone_number (Optional[str]): The phone number. + snippet (Optional[Snippet]): The snippet. + description (Optional[str]): The description. + view (Optional[Union[Camera, LookAt]]): The view. + times (Optional[Union[TimeSpan, TimeStamp]]): The times. + style_url (Optional[StyleUrl]): The style URL. + styles (Optional[Iterable[Union[Style, StyleMap]]]): The styles. + region (Optional[Region]): The region. + extended_data (Optional[ExtendedData]): The extended data. + features (Optional[List[_Feature]]): The list of features. + schemata (Optional[Iterable[Schema]]): The schemata. + **kwargs (Any): Additional keyword arguments. + + Returns: + ------- + None + + """ super().__init__( ns=ns, name_spaces=name_spaces, @@ -255,6 +291,20 @@ def __repr__(self) -> str: ) def get_style_by_url(self, style_url: str) -> Optional[Union[Style, StyleMap]]: + """ + Get a style by URL. + + Parameters + ---------- + style_url : str + The URL of the style. + + Returns + ------- + Optional[Union[Style, StyleMap]] + The style object if found, otherwise None. + + """ id_ = urlparse.urlparse(style_url).fragment return next((style for style in self.styles if style.id == id_), None) From 401c8e97d12b6b8f6d35bfe786c171811066d80a Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Thu, 18 Jul 2024 17:03:51 +0100 Subject: [PATCH 04/12] better docstrings and ruff fixes --- fastkml/atom.py | 6 +- fastkml/containers.py | 8 +- fastkml/data.py | 247 +++++++++++++++++++++++++++++--- fastkml/enums.py | 16 ++- fastkml/features.py | 322 +++++++++++++++++++++++++++++++----------- fastkml/kml_base.py | 2 +- pyproject.toml | 2 + 7 files changed, 484 insertions(+), 119 deletions(-) diff --git a/fastkml/atom.py b/fastkml/atom.py index 3e02bd5d..592d667f 100644 --- a/fastkml/atom.py +++ b/fastkml/atom.py @@ -90,13 +90,13 @@ class Link(_AtomObject): title: Optional[str] length: Optional[int] - def __init__( # noqa: PLR0913 + 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, # noqa: A002 + type: Optional[str] = None, hreflang: Optional[str] = None, title: Optional[str] = None, length: Optional[int] = None, @@ -292,7 +292,7 @@ class _Person(_AtomObject): uri: Optional[str] email: Optional[str] - def __init__( # noqa: PLR0913 + def __init__( self, ns: Optional[str] = None, name_spaces: Optional[Dict[str, str]] = None, diff --git a/fastkml/containers.py b/fastkml/containers.py index 12ed79ef..1b3385db 100644 --- a/fastkml/containers.py +++ b/fastkml/containers.py @@ -73,11 +73,11 @@ class _Container(_Feature): features: List[_Feature] - def __init__( # noqa: PLR0913 + def __init__( self, ns: Optional[str] = None, name_spaces: Optional[Dict[str, str]] = None, - id: Optional[str] = None, # noqa: A002 + id: Optional[str] = None, target_id: Optional[str] = None, name: Optional[str] = None, visibility: Optional[bool] = None, @@ -177,11 +177,11 @@ class Document(_Container): schemata: List[Schema] - def __init__( # noqa: PLR0913 + def __init__( self, ns: Optional[str] = None, name_spaces: Optional[Dict[str, str]] = None, - id: Optional[str] = None, # noqa: A002 + id: Optional[str] = None, target_id: Optional[str] = None, name: Optional[str] = None, visibility: Optional[bool] = None, diff --git a/fastkml/data.py b/fastkml/data.py index 6dc1bb24..1b2c5822 100644 --- a/fastkml/data.py +++ b/fastkml/data.py @@ -16,7 +16,7 @@ """ Add Custom Data. -https://developers.google.com/kml/documentation/extendeddata#example +https://developers.google.com/kml/documentation/extendeddata """ import logging from typing import Any @@ -91,6 +91,26 @@ def __init__( display_name: Optional[str] = None, **kwargs: Any, ) -> None: + """ + Initialize a new instance of the Data class. + + Args: + ---- + ns (Optional[str]): The namespace of the data. + name_spaces (Optional[Dict[str, str]]): + The dictionary of namespace prefixes and URIs. + id (Optional[str]): The ID of the data. + target_id (Optional[str]): The target ID of the data. + name (Optional[str]): The name of the data. + type (Optional[DataType]): The type of the data. + display_name (Optional[str]): The display name of the data. + **kwargs (Any): Additional keyword arguments. + + Returns: + ------- + None + + """ super().__init__( ns=ns, name_spaces=name_spaces, @@ -103,7 +123,14 @@ def __init__( self.display_name = display_name def __repr__(self) -> str: - """Create a string (c)representation for SimpleField.""" + """ + Return a string representation of the SimpleField object. + + Returns + ------- + str: A string representation of the SimpleField object. + + """ return ( f"{self.__class__.__module__}.{self.__class__.__name__}(" f"ns={self.ns!r}, " @@ -118,6 +145,14 @@ def __repr__(self) -> str: ) def __bool__(self) -> bool: + """ + Check if the object is considered True or False. + + Returns + ------- + bool: True if both the name and type are non-empty, False otherwise. + + """ return bool(self.name) and bool(self.type) @@ -180,6 +215,32 @@ def __init__( fields: Optional[Iterable[SimpleField]] = None, **kwargs: Any, ) -> None: + """ + Initialize a Schema object. + + Parameters + ---------- + ns : str, optional + The namespace of the schema. + name_spaces : dict[str, str], optional + The dictionary of namespace prefixes and URIs. + id : str, optional + The unique identifier for the schema. + target_id : str, optional + The target identifier for the schema. + name : str, optional + The name of the schema. + fields : Iterable[SimpleField], optional + The list of fields in the schema. + **kwargs : Any + Additional keyword arguments. + + Raises + ------ + KMLSchemaError + If the id is not provided. + + """ if id is None: msg = "Id is required for schema" raise KMLSchemaError(msg) @@ -194,7 +255,15 @@ def __init__( self.fields = list(fields) if fields else [] def __repr__(self) -> str: - """Create a string (c)representation for Schema.""" + """ + Return a string representation of the Schema object. + + Returns + ------- + str + The string representation of the Schema object. + + """ return ( f"{self.__class__.__module__}.{self.__class__.__name__}(" f"ns={self.ns!r}, " @@ -208,7 +277,15 @@ def __repr__(self) -> str: ) def append(self, field: SimpleField) -> None: - """Append a field.""" + """ + Append a field to the schema. + + Parameters + ---------- + field : SimpleField + The field to be appended. + + """ self.fields.append(field) @@ -254,6 +331,26 @@ def __init__( display_name: Optional[str] = None, **kwargs: Any, ) -> None: + """ + Initialize a new instance of the Data class. + + Args: + ---- + ns (Optional[str]): The namespace of the data. + name_spaces (Optional[Dict[str, str]]): + The dictionary of namespace prefixes and URIs. + id (Optional[str]): The ID of the data. + target_id (Optional[str]): The target ID of the data. + name (Optional[str]): The name of the data. + value (Optional[str]): The value of the data. + display_name (Optional[str]): The display name of the data. + **kwargs (Any): Additional keyword arguments. + + Returns: + ------- + None + + """ super().__init__( ns=ns, name_spaces=name_spaces, @@ -266,7 +363,15 @@ def __init__( self.display_name = display_name def __repr__(self) -> str: - """Create a string (c)representation for Data.""" + """ + Create a string representation for Data. + + Returns + ------- + str + The string representation of the Data object. + + """ return ( f"{self.__class__.__module__}.{self.__class__.__name__}(" f"ns={self.ns!r}, " @@ -281,6 +386,15 @@ def __repr__(self) -> str: ) def __bool__(self) -> bool: + """ + Check if the Data object is truthy. + + Returns + ------- + bool + True if the Data object has a name and a non-None value, False otherwise. + + """ return bool(self.name) and self.value is not None @@ -334,6 +448,21 @@ def __init__( value: Optional[str] = None, **kwargs: Any, ) -> None: + """ + Initialize a SimpleData object. + + Args: + ---- + ns (Optional[str]): The namespace of the object. + name_spaces (Optional[Dict[str, str]]): + The dictionary of namespace prefixes and URIs. + id (Optional[str]): The ID of the object. + target_id (Optional[str]): The target ID of the object. + name (Optional[str]): The name of the object. + value (Optional[str]): The value of the object. + **kwargs: Additional keyword arguments. + + """ super().__init__( ns=ns, name_spaces=name_spaces, @@ -345,7 +474,14 @@ def __init__( self.value = value def __repr__(self) -> str: - """Create a string (c)representation for SimpleData.""" + """ + Return a string representation of the SimpleData object. + + Returns + ------- + str: The string representation of the SimpleData object. + + """ return ( f"{self.__class__.__module__}.{self.__class__.__name__}(" f"ns={self.ns!r}, " @@ -359,6 +495,14 @@ def __repr__(self) -> str: ) def __bool__(self) -> bool: + """ + Check if the SimpleData object is truthy. + + Returns + ------- + bool: True if the name and the value attribute are set, False otherwise. + + """ return bool(self.name) and self.value is not None @@ -388,15 +532,13 @@ def __bool__(self) -> bool: class SchemaData(_BaseObject): """ - - This element is used in conjunction with to add typed + Represents the SchemaData element in KML. + + The SchemaData element is used in conjunction with Schema to add typed custom data to a KML Feature. The Schema element (identified by the schemaUrl attribute) declares the custom data type. The actual data objects ("instances" of the custom data) are defined using the SchemaData element. - The can be a full URL, a reference to a Schema ID defined - in an external KML file, or a reference to a Schema ID defined - in the same KML file. """ schema_url: Optional[str] @@ -412,6 +554,25 @@ def __init__( data: Optional[Iterable[SimpleData]] = None, **kwargs: Any, ) -> None: + """ + Initialize a new instance of the Data class. + + Args: + ---- + ns (Optional[str]): The namespace for the data. + name_spaces (Optional[Dict[str, str]]): + The dictionary of namespace prefixes and URIs. + id (Optional[str]): The ID of the data. + target_id (Optional[str]): The target ID of the data. + schema_url (Optional[str]): The URL of the schema for the data. + data (Optional[Iterable[SimpleData]]): The iterable of SimpleData objects. + **kwargs (Any): Additional keyword arguments. + + Returns: + ------- + None + + """ super().__init__( ns=ns, name_spaces=name_spaces, @@ -423,7 +584,7 @@ def __init__( self.data = list(data) if data else [] def __repr__(self) -> str: - """Create a string (c)representation for SchemaData.""" + """Create a string representation for SchemaData.""" return ( f"{self.__class__.__module__}.{self.__class__.__name__}(" f"ns={self.ns!r}, " @@ -437,9 +598,26 @@ def __repr__(self) -> str: ) def __bool__(self) -> bool: + """ + Check if the SchemaData object contains data. + + Returns + ------- + bool: True if the SchemaData object contains data and a + schema URL, False otherwise. + + """ return bool(self.data) and bool(self.schema_url) def append_data(self, data: SimpleData) -> None: + """ + Append a SimpleData object to the SchemaData. + + Args: + ---- + data (SimpleData): The SimpleData object to be appended. + + """ self.data.append(data) @@ -468,13 +646,7 @@ def append_data(self, data: SimpleData) -> None: class ExtendedData(_BaseObject): - """ - Represents a list of untyped name/value pairs. See docs: - - -> 'Adding Untyped Name/Value Pairs' - https://developers.google.com/kml/documentation/extendeddata - - """ + """Represents a list of untyped name/value pairs.""" elements: List[Union[Data, SchemaData]] @@ -487,6 +659,25 @@ def __init__( elements: Optional[Iterable[Union[Data, SchemaData]]] = None, **kwargs: Any, ) -> None: + """ + Initialize a new instance of the Data class. + + Args: + ---- + ns (Optional[str]): The namespace for the data. + name_spaces (Optional[Dict[str, str]]): + The dictionary of namespace prefixes and URIs. + id (Optional[str]): The ID of the data. + target_id (Optional[str]): The target ID of the data. + elements (Optional[Iterable[Union[Data, SchemaData]]]): + The iterable of data elements. + **kwargs (Any): Additional keyword arguments. + + Returns: + ------- + - None + + """ super().__init__( ns=ns, name_spaces=name_spaces, @@ -497,7 +688,14 @@ def __init__( self.elements = list(elements) if elements else [] def __repr__(self) -> str: - """Create a string (c)representation for ExtendedData.""" + """ + Return a string representation of the ExtendedData object. + + Returns + ------- + str: A string representation of the ExtendedData object. + + """ return ( f"{self.__class__.__module__}.{self.__class__.__name__}(" f"ns={self.ns!r}, " @@ -510,6 +708,15 @@ def __repr__(self) -> str: ) def __bool__(self) -> bool: + """ + Check if the object has any elements. + + Returns + ------- + bool + True if the object has elements, False otherwise. + + """ return bool(self.elements) diff --git a/fastkml/enums.py b/fastkml/enums.py index 8200b1e7..407efed5 100644 --- a/fastkml/enums.py +++ b/fastkml/enums.py @@ -54,14 +54,16 @@ class MyEnum(RelaxedEnum): @classmethod def _missing_(cls, value: object) -> "RelaxedEnum": - assert isinstance(value, str) + assert isinstance(value, str) # noqa: S101 value = value.lower() for member in cls: - assert isinstance(member.value, str) + assert isinstance(member.value, str) # noqa: S101 if member.value.lower() == value.lower(): logger.warning( - f"{cls.__name__}: " - f"Found case-insensitive match for {value} in {member.value}", + "%s: Found case-insensitive match for %s in %r", + cls.__name__, + value, + member.value, ) return member msg = ( @@ -140,6 +142,8 @@ class AltitudeMode(RelaxedEnum): @unique class DataType(RelaxedEnum): + """Data type for SimpleField in extended data.""" + string = "string" int_ = "int" uint = "uint" @@ -251,9 +255,7 @@ class Units(RelaxedEnum): @unique class PairKey(RelaxedEnum): - """ - Key for Pair. - """ + """Key for Pair.""" normal = "normal" highlight = "highlight" diff --git a/fastkml/features.py b/fastkml/features.py index 52f5bad0..d016f296 100644 --- a/fastkml/features.py +++ b/fastkml/features.py @@ -83,6 +83,7 @@ class Snippet(_XMLObject): """ A short description of the feature. + In Google Earth, this description is displayed in the Places panel under the name of the feature. If a Snippet is not supplied, the first two lines of the @@ -107,12 +108,40 @@ def __init__( max_lines: Optional[int] = None, **kwargs: Any, ) -> None: + """ + Initialize a Feature object. + + Args: + ---- + ns : str, optional + The namespace for the feature. + name_spaces : dict[str, str], optional + A dictionary of namespace prefixes and URIs. + text : str, optional + The text content of the feature. + max_lines : int, optional + The maximum number of lines for the feature. + **kwargs : Any + Additional keyword arguments. + + Returns: + ------- + None + + """ super().__init__(ns=ns, name_spaces=name_spaces, **kwargs) self.text = text self.max_lines = max_lines def __repr__(self) -> str: - """Create a string (c)representation for Snippet.""" + """ + Create a string representation for Snippet. + + Returns + ------- + str: The string representation of the Snippet object. + + """ return ( f"{self.__class__.__module__}.{self.__class__.__name__}(" f"ns={self.ns!r}, " @@ -124,6 +153,15 @@ def __repr__(self) -> str: ) def __bool__(self) -> bool: + """ + Check if the feature has text. + + Returns + ------- + bool + True if the feature has text, False otherwise. + + """ return bool(self.text) @@ -153,102 +191,29 @@ def __bool__(self) -> bool: class _Feature(TimeMixin, _BaseObject): """ - abstract element; do not create - subclasses are: - * Container (Document, Folder) - * Placemark - * Overlay - * NetworkLink. + Abstract element representing a feature in KML. + + Subclasses are: + - Container (Document, Folder) + - Placemark + - Overlay + - NetworkLink. """ name: Optional[str] - # User-defined text displayed in the 3D viewer as the label for the - # object (for example, for a Placemark, Folder, or NetworkLink). - visibility: Optional[bool] - # Boolean value. Specifies whether the feature is drawn in the 3D - # viewer when it is initially loaded. In order for a feature to be - # visible, the tag of all its ancestors must also be - # set to 1. - isopen: Optional[bool] - # Boolean value. Specifies whether a Document or Folder appears - # closed or open when first loaded into the Places panel. - # 0=collapsed (the default), 1=expanded. - atom_author: Optional[atom.Author] - # KML 2.2 supports new elements for including data about the author - # and related website in your KML file. This information is displayed - # in geo search results, both in Earth browsers such as Google Earth, - # and in other applications such as Google Maps. - atom_link: Optional[atom.Link] - # Specifies the URL of the website containing this KML or KMZ file. - address: Optional[str] - # A string value representing an unstructured address written as a - # standard street, city, state address, and/or as a postal code. - # You can use the
tag to specify the location of a point - # instead of using latitude and longitude coordinates. - phone_number: Optional[str] - # A string value representing a telephone number. - # This element is used by Google Maps Mobile only. - snippet: Optional[Snippet] - # _snippet is either a tuple of a string Snippet.text and an integer - # Snippet.maxLines or a string - # - # A short description of the feature. In Google Earth, this - # description is displayed in the Places panel under the name of the - # feature. If a Snippet is not supplied, the first two lines of - # the are used. In Google Earth, if a Placemark - # contains both a description and a Snippet, the appears - # beneath the Placemark in the Places panel, and the - # appears in the Placemark's description balloon. This tag does not - # support HTML markup. has a maxLines attribute, an integer - # that specifies the maximum number of lines to display. - description: Optional[str] - # User-supplied content that appears in the description balloon. - style_url: Optional[StyleUrl] - # URL of a