diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9947b93c..d3629c79 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -43,7 +43,7 @@ repos: hooks: - id: absolufy-imports - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.7.3' + rev: 'v0.7.4' hooks: - id: ruff - id: ruff-format diff --git a/README.rst b/README.rst index 4c8f58b2..3fe99453 100644 --- a/README.rst +++ b/README.rst @@ -116,7 +116,7 @@ Optional .. code-block:: bash - pip install --pre "fastkml[lxml]" + pip install "fastkml[lxml]" Limitations =========== diff --git a/docs/co2growth.gif b/docs/co2growth.gif new file mode 100644 index 00000000..9f84a28d Binary files /dev/null and b/docs/co2growth.gif differ diff --git a/docs/create_kml_files.rst b/docs/create_kml_files.rst index e81a5538..f722aa20 100644 --- a/docs/create_kml_files.rst +++ b/docs/create_kml_files.rst @@ -6,6 +6,14 @@ Read a shapefile and build a 3D KML visualization. This example shows how to read a shapefile and build a 3D KML visualization from it. +.. image:: co2-per-capita-2020.jpg + :alt: CO2 emissions per capita in 2020 + :align: center + :width: 800px + :target: https://ion.cesium.com/stories/viewer/?id=a3cf93bb-bbb8-488b-8643-09c037ec12b8 + +(click on the image to see the KML visualization in Cesium Ion) + You will need to install `pyshp `_ (``pip install pyshp``). For this example we will use the @@ -108,12 +116,6 @@ Finally, we create the KML object and write it to a file: The resulting KML file can be opened in Google Earth or any other KML viewer. -.. image:: co2-per-capita-2020.jpg - :alt: CO2 emissions per capita in 2020 - :align: center - :width: 800px - :target: https://ion.cesium.com/stories/viewer/?id=a3cf93bb-bbb8-488b-8643-09c037ec12b8 - Build an animated over time KML visualization ---------------------------------------------- @@ -122,6 +124,14 @@ This example shows how to build an animated KML visualization over time. We will use the same data as in the previous example, but this time we will create a KML file that shows the CO2 emissions accumulating from 1995 to 2022. +.. image:: co2growth.gif + :alt: CO2 emissions per capita growth + :align: center + :width: 800px + :target: https://ion.cesium.com/stories/viewer/?id=602c8c64-72aa-4c57-8a01-752b6fbc62d0 + +(click on the image to see the full visualization in Cesium Ion) + First we import the necessary modules: .. code-block:: pycon diff --git a/docs/fastkml.rst b/docs/fastkml.rst index e752b965..96553566 100644 --- a/docs/fastkml.rst +++ b/docs/fastkml.rst @@ -4,27 +4,76 @@ Reference Guide .. automodule:: fastkml +fastkml.kml +------------------ -fastkml.about --------------------- - -.. automodule:: fastkml.about +.. automodule:: fastkml.kml :members: :undoc-members: :show-inheritance: -fastkml.atom + +fastkml.base ------------------- -.. automodule:: fastkml.atom +.. automodule:: fastkml.base :members: :undoc-members: :show-inheritance: -fastkml.base +.. autoclass:: fastkml.base._XMLObject + :members: etree_element, _get_kwargs, to_string, from_string, validate, __init__ + :undoc-members: + :show-inheritance: + +fastkml.registry +----------------------- + +.. automodule:: fastkml.registry + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: fastkml.registry::Registry + :members: register, get + :undoc-members: + :show-inheritance: + +fastkml.kml\_base +------------------------ + +.. automodule:: fastkml.kml_base + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: fastkml.kml_base::_BaseObject + :members: __init__ + :undoc-members: + :show-inheritance: + +fastkml.helpers +---------------------- + +.. automodule:: fastkml.helpers + :members: + :undoc-members: + :show-inheritance: + +fastkml.about +-------------------- + +.. automodule:: fastkml.about + :members: + :undoc-members: + :show-inheritance: + + .. autodata:: __version__ + +fastkml.atom ------------------- -.. automodule:: fastkml.base +.. automodule:: fastkml.atom :members: :undoc-members: :show-inheritance: @@ -93,30 +142,6 @@ fastkml.gx :undoc-members: :show-inheritance: -fastkml.helpers ----------------------- - -.. automodule:: fastkml.helpers - :members: - :undoc-members: - :show-inheritance: - -fastkml.kml ------------------- - -.. automodule:: fastkml.kml - :members: - :undoc-members: - :show-inheritance: - -fastkml.kml\_base ------------------------- - -.. automodule:: fastkml.kml_base - :members: - :undoc-members: - :show-inheritance: - fastkml.links -------------------- @@ -141,13 +166,6 @@ fastkml.overlays :undoc-members: :show-inheritance: -fastkml.registry ------------------------ - -.. automodule:: fastkml.registry - :members: - :undoc-members: - :show-inheritance: fastkml.styles --------------------- diff --git a/fastkml/about.py b/fastkml/about.py index 578573fc..ce656dad 100644 --- a/fastkml/about.py +++ b/fastkml/about.py @@ -19,6 +19,7 @@ The only purpose of this module is to provide a version number for the package. """ -__version__ = "1.0.0rc2" +__version__ = "1.0.0" +"""Fastkml version number.""" __all__ = ["__version__"] diff --git a/fastkml/base.py b/fastkml/base.py index 88e7c0dd..ca2c7c21 100644 --- a/fastkml/base.py +++ b/fastkml/base.py @@ -14,7 +14,22 @@ # 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 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 @@ -127,6 +142,18 @@ def etree_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 @@ -280,6 +307,31 @@ def _get_kwargs( """ 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 diff --git a/fastkml/enums.py b/fastkml/enums.py index 75de9ebb..df3f22e3 100644 --- a/fastkml/enums.py +++ b/fastkml/enums.py @@ -26,6 +26,22 @@ from enum import Enum from enum import unique +__all__ = [ + "AltitudeMode", + "ColorMode", + "DataType", + "DateTimeResolution", + "DisplayMode", + "GridOrigin", + "PairKey", + "RefreshMode", + "RelaxedEnum", + "Shape", + "Units", + "Verbosity", + "ViewRefreshMode", +] + logger = logging.getLogger(__name__) @@ -40,14 +56,15 @@ class RelaxedEnum(Enum): Usage: To use this enum, simply subclass `RelaxedEnum` and define your enum values. - Example: - ------- - class MyEnum(RelaxedEnum): - VALUE1 = "value1" - VALUE2 = "value2" + Example:: + + class AltitudeMode(RelaxedEnum): + clamp_to_ground = "clampToGround" + relative_to_ground = "relativeToGround" + absolute = "absolute" - my_value = MyEnum("VALUE1") # Case-insensitive match - print(my_value) # Output: MyEnum.VALUE1 + my_value = AltitudeMode("CLAMPTOGROUND") # Case-insensitive match + print(my_value) # Output: AltitudeMode.clamp_to_ground The subclass must define all values as strings. @@ -102,33 +119,33 @@ class AltitudeMode(RelaxedEnum): Possible values are - clampToGround - (default) Indicates to ignore an altitude specification - (for example, in the tag). + (for example, in the tag). - relativeToGround - Sets the altitude of the element relative to the actual - ground elevation of a particular location. - For example, if the ground elevation of a location is exactly at sea level - and the altitude for a point is set to 9 meters, - then the elevation for the icon of a point placemark elevation is 9 meters - with this mode. - However, if the same coordinate is set over a location where the ground - elevation is 10 meters above sea level, then the elevation of the coordinate - is 19 meters. - A typical use of this mode is for placing telephone poles or a ski lift. + ground elevation of a particular location. + For example, if the ground elevation of a location is exactly at sea level + and the altitude for a point is set to 9 meters, + then the elevation for the icon of a point placemark elevation is 9 meters + with this mode. + However, if the same coordinate is set over a location where the ground + elevation is 10 meters above sea level, then the elevation of the coordinate + is 19 meters. + A typical use of this mode is for placing telephone poles or a ski lift. - absolute - Sets the altitude of the coordinate relative to sea level, - regardless of the actual elevation of the terrain beneath the element. - For example, if you set the altitude of a coordinate to 10 meters with an - absolute altitude mode, the icon of a point placemark will appear to be at - ground level if the terrain beneath is also 10 meters above sea level. - If the terrain is 3 meters above sea level, the placemark will appear elevated - above the terrain by 7 meters. - A typical use of this mode is for aircraft placement. + regardless of the actual elevation of the terrain beneath the element. + For example, if you set the altitude of a coordinate to 10 meters with an + absolute altitude mode, the icon of a point placemark will appear to be at + ground level if the terrain beneath is also 10 meters above sea level. + If the terrain is 3 meters above sea level, the placemark will appear elevated + above the terrain by 7 meters. + A typical use of this mode is for aircraft placement. - relativeToSeaFloor - Interprets the altitude as a value in meters above the - sea floor. - If the point is above land rather than sea, the altitude will be interpreted - as being above the ground. + sea floor. + If the point is above land rather than sea, the altitude will be interpreted + as being above the ground. - clampToSeaFloor - The altitude specification is ignored, and the point will be - positioned on the sea floor. - If the point is on land rather than at sea, the point will be positioned on - the ground. + positioned on the sea floor. + If the point is on land rather than at sea, the point will be positioned on + the ground. The Values relativeToSeaFloor and clampToSeaFloor are not part of the KML definition but of the a KML extension in the Google extension namespace, diff --git a/fastkml/helpers.py b/fastkml/helpers.py index 85a888e8..22010293 100644 --- a/fastkml/helpers.py +++ b/fastkml/helpers.py @@ -13,7 +13,22 @@ # 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 for fastkml.""" +""" +Helper functions for fastkml. + +The helpers module acts as a bridge between the abstract registry definitions and the +concrete XML operations performed by ``_XMLObject``. + +- Provides utility functions for XML parsing and serialization referenced in the + registry for handling different data types. +- Implements ``get_kwarg`` and ``set_element`` functions used by ``_XMLObject``. +- Offers helper functions for common operations like text handling and type conversions. + +``_XMLObject`` uses these helpers indirectly through the registry. +The registry references these helper functions for attribute parsing and serialization. +They form the implementation layer for the declarative approach defined by the registry. + +""" import logging from enum import Enum diff --git a/fastkml/kml.py b/fastkml/kml.py index a0314c11..0886c6ba 100644 --- a/fastkml/kml.py +++ b/fastkml/kml.py @@ -153,12 +153,10 @@ def etree_element( Return an Element object representing the KML element. Args: - ---- precision (Optional[int]): The precision used for floating-point values. verbosity (Verbosity): The verbosity level for generating the KML element. Returns: - ------- Element: The etree Element object representing the KML element. """ @@ -211,14 +209,11 @@ def parse( Parse a KML file and return a KML object. Args: - ---- - file: The file to parse. - Can be a file path (str or Path), or a file-like object. + file: The file to parse. Can be a file path or a file-like object. Keyword Args: - ------------ ns (Optional[str]): The namespace of the KML file. - If not provided, it will be inferred from the root element. + If not provided, it will be inferred from the root element. name_spaces (Optional[Dict[str, str]]): Additional namespaces. strict (bool): Whether to enforce strict parsing rules. Defaults to True. validate (Optional[bool]): Whether to validate the file against the schema. @@ -257,10 +252,9 @@ def write( Write KML to a file. Args: - ---- file_path: The file name where to save the file. Can be any string value - prettyprint : bool, default=True + prettyprint (bool): default=True Whether to pretty print the XML. precision (Optional[int]): The precision used for floating-point values. verbosity (Verbosity): The verbosity level for generating the KML element. diff --git a/fastkml/kml_base.py b/fastkml/kml_base.py index adaa5cad..3e86bf25 100644 --- a/fastkml/kml_base.py +++ b/fastkml/kml_base.py @@ -14,7 +14,21 @@ # 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 KML base class. + +It adapts the generic XML functionality of ``_XMLObject`` to the specific needs of KML +elements, providing a foundation for all KML-specific classes in fastkml. + +- Serve as the base class for all KML objects +- Implement common KML attributes like 'id' and 'target_id' +- Set the default namespace to KML +- Provide KML-specific initialization logic +- Act as an intermediary between _XMLObject and concrete KML classes +- Ensure consistent handling of basic KML properties across all elements +- Facilitate KML-specific functionality while inheriting generic XML capabilities + +""" from typing import Any from typing import Dict diff --git a/fastkml/registry.py b/fastkml/registry.py index e7604908..96e53bb1 100644 --- a/fastkml/registry.py +++ b/fastkml/registry.py @@ -13,7 +13,22 @@ # 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 -"""Registry for XML objects.""" +""" +Registry for XML objects. + +The Registry is used to store and retrieve information about XML objects. +This approach allows for flexible, declarative mapping between XML and Python objects, +with the registry acting as a central configuration for these mappings. + +Direct ``Registry`` class use is typically only for library internals or advanced +customization. For normal usage, stick with the ``registry`` instance: + +- The library is designed around this global instance. +- Ensures all parts of the library use the same registry. +- Pre-populated with standard KML mappings. +- Singleton pattern: Avoids multiple conflicting registries. + +""" from dataclasses import dataclass from typing import TYPE_CHECKING @@ -63,7 +78,22 @@ def __call__( @dataclass(frozen=True) class RegistryItem: - """A registry item.""" + """ + A registry item. + + The RegistryItem class is a dataclass that represents a single mapping between an + XML object and a Python object. It contains the following fields: + + - ``ns_ids``: A tuple of namespace identifiers that the mapping applies to. + - ``classes``: A tuple of Python classes that the mapping applies to. + - ``attr_name``: The name of the attribute on the Python object that corresponds to + the XML object. + - ``get_kwarg``: A function that retrieves keyword arguments for the Python object. + - ``type``: The type of the XML object. + - ``node_name``: The name of the XML node that the mapping applies to. + - ``default``: An optional default value for the Python object attribute. + + """ ns_ids: Tuple[str, ...] classes: Tuple[Type[object], ...] @@ -75,7 +105,26 @@ class RegistryItem: class Registry: - """A registry of XML objects.""" + """ + A registry of XML objects. + + The registry acts as a configuration hub, allowing the library to dynamically handle + various KML elements and their attributes without hardcoding the logic into each + class. + + The purpose of the registry is to: + + - Centralize XML mapping configuration for KML objects. + - Define attribute-to-element/attribute mappings. + - Specify parsing and serialization functions for each attribute. + - Support inheritance in XML mappings. + - Provide a flexible, declarative approach to XML handling. + - Decouple XML parsing/serialization logic from class definitions. + - Allow easy addition or modification of XML mappings. + - Enable consistent handling of attributes across different KML classes. + - Facilitate extensibility and maintainability of the library. + + """ _registry: Dict[Type["_XMLObject"], List[RegistryItem]] @@ -93,13 +142,43 @@ def __repr__(self) -> str: ) def register(self, cls: Type["_XMLObject"], item: RegistryItem) -> None: - """Register a class.""" + """ + Register a class. + + Add a new RegistryItem to the registry for a specific class. + + - Appends the item to an existing list if the class is already registered. + - Creates a new list with the item if it's the first for that class. + - Associates XML parsing/serialization rules with a class attribute. + - Defines how a specific attribute should be handled in XML operations. + - Allows for multiple registrations per class, supporting complex mappings. + - Is called during library initialization to set up KML mappings. + + This is the primary way to configure how different KML elements and their + attributes are processed in fastkml. + + """ existing = self._registry.get(cls, []) existing.append(item) self._registry[cls] = existing def get(self, cls: Type["_XMLObject"]) -> List[RegistryItem]: - """Get a class by name.""" + """ + Get the registry items for a class and its ancestors. + + The get method of the registry, in conjunction with _XMLObject: + + - Retrieves all registered items for a given class and its ancestors. + - Supports inheritance in XML mappings. + - Allows ``_XMLObject`` to dynamically determine how to parse/serialize + attributes. + - Enables flexible XML handling without hardcoding in each class. + - Facilitates polymorphic behavior in XML parsing and serialization. + + It allows ``_XMLObject`` to handle different KML elements consistently while + respecting their inheritance structure. + + """ parents = reversed(cls.__mro__[:-1]) items = [] for parent in parents: diff --git a/pyproject.toml b/pyproject.toml index 674d2541..c525ae04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ authors = [ { email = "christian.ledermann@gmail.com", name = "Christian Ledermann" }, ] classifiers = [ - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", "Operating System :: OS Independent", @@ -179,6 +179,7 @@ include = [ [tool.rstcheck] ignore_directives = [ + "autoclass", "automodule", ] report_level = "WARNING" @@ -227,15 +228,6 @@ select = [ "S101", "SLF001", ] -"tests/oldunit_test.py" = [ - "D100", - "D200", - "D401", - "E501", - "FIX", - "ICN001", - "TD", -] "tests/repr_eq_test.py" = [ "E501", ] diff --git a/tests/repr_eq_test.py b/tests/repr_eq_test.py index 8297258f..20b0eb70 100644 --- a/tests/repr_eq_test.py +++ b/tests/repr_eq_test.py @@ -1918,7 +1918,6 @@ def test_eq_str_round_trip(self) -> None: assert str(self.clean_doc) == str(new_doc) assert new_doc == self.clean_doc - # srict equality is not a given new_doc == self.clean_doc class TestReprLxml(Lxml, TestRepr):