diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index e49b889f..5b2d46cb 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13-dev'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13-dev', '3.14-dev'] steps: - uses: actions/checkout@v4 @@ -29,7 +29,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13-dev'] steps: - uses: actions/checkout@v4 @@ -87,10 +87,10 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip wheel - pip install -e ".[typing, complexity, linting]" + pip install -e ".[typing, complexity, linting, tests]" - name: Typecheck run: | - mypy fastkml + mypy fastkml tests - name: Linting run: | flake8 fastkml examples docs diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ccf4285a..53344162 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: hooks: - id: pyprojectsort - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.18 + rev: v0.20.2 hooks: - id: validate-pyproject - repo: https://github.com/ikamensh/flynt/ @@ -37,12 +37,12 @@ repos: hooks: - id: absolufy-imports - repo: https://github.com/asottile/pyupgrade - rev: v3.16.0 + rev: v3.17.0 hooks: - id: pyupgrade args: ["--py3-plus", "--py37-plus"] - repo: https://github.com/psf/black - rev: 24.4.2 + rev: 24.8.0 hooks: - id: black - repo: https://github.com/pycqa/isort @@ -50,11 +50,11 @@ repos: hooks: - id: isort - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.4.9' + rev: 'v0.6.8' hooks: - id: ruff - repo: https://github.com/PyCQA/flake8 - rev: 7.1.0 + rev: 7.1.1 hooks: - id: flake8 - repo: https://github.com/pre-commit/pygrep-hooks @@ -70,18 +70,22 @@ repos: - id: rst-inline-touching-normal - id: text-unicode-replacement-char - repo: https://github.com/rstcheck/rstcheck - rev: "v6.2.0" + rev: "v6.2.4" hooks: - id: rstcheck - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.10.0 + rev: v1.11.2 hooks: - id: mypy additional_dependencies: [pygeoif>=1.4, arrow, pytest] - repo: https://github.com/adamchainz/blacken-docs - rev: "1.16.0" + rev: "1.18.0" hooks: - id: blacken-docs + - repo: https://github.com/crate-ci/typos + rev: v1.25.0 + hooks: + - id: typos # - repo: https://github.com/Lucas-C/pre-commit-hooks-markup # rev: v1.0.1 # hooks: diff --git a/README.rst b/README.rst index 700145ea..6db81743 100644 --- a/README.rst +++ b/README.rst @@ -117,9 +117,9 @@ Optional Limitations =========== -*Tesselate*, *Extrude* and *Altitude Mode* are assigned to a Geometry or +*Tessellate*, *Extrude* and *Altitude Mode* are assigned to a Geometry or Geometry collection (MultiGeometry). You cannot assign different values of -*Tesselate*, *Extrude* or *Altitude Mode* on parts of a MultiGeometry. +*Tessellate*, *Extrude* or *Altitude Mode* on parts of a MultiGeometry. Currently, the only major feature missing for the full Google Earth experience is the `gx extension diff --git a/_typos.toml b/_typos.toml new file mode 100644 index 00000000..a1cb930e --- /dev/null +++ b/_typos.toml @@ -0,0 +1,6 @@ +[default.extend-words] +lod = "lod" +Lod = "Lod" + +[files] +extend-exclude = ["tests/ogc_conformance/data/kml/*.kml"] diff --git a/docs/HISTORY.rst b/docs/HISTORY.rst index e5e3255c..4d742416 100644 --- a/docs/HISTORY.rst +++ b/docs/HISTORY.rst @@ -80,7 +80,7 @@ Changelog ----------------- - specify minor python versions tested with Travis CI -- add support for tesselation, altitudeMode and extrude to Geometries +- add support for tessellation, altitudeMode and extrude to Geometries - move implementation of geometry from kml.Placemark to geometry.Geometry - add support for heterogenous GeometryCollection - python 3 compatible diff --git a/docs/contributing.rst b/docs/contributing.rst index 319aaa39..2ade1e87 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -11,76 +11,70 @@ especially in the following ways: * Commenting on open issues and pull requests * Suggesting new features +Setting Up Your Environment +--------------------------- -Pull Requests -------------- +Fork the repository and clone your fork to your local machine: -Start by submitting a pull request on GitHub against the ``develop`` branch of the -repository. Your pull request should provide a good description of the change -you are making, and/or the bug that you are fixing. +.. code-block:: bash + git clone https://github.com/yourusername/fastkml.git + cd fastkml + git checkout develop -Running Tests Locally ---------------------- +Next, set up a virtual environment. This helps to manage dependencies and avoid conflicts: -You can make use of tox_ >= 1.8 to test the entire matrix of options: +.. code-block:: bash -* with / without lxml -* py36,py37,py38,py39 + python3 -m venv .venv + source venv/bin/activate # On Windows use `venv\Scripts\activate` -as well as pep8 style checking in a single call (this approximates what happens -when the package is run through Travis-CI) +Then, install the required packages: -.. code-block:: python +.. code-block:: bash - # Install tox - pip install tox>=1.8 + pip install -e ".[dev]" - # Run tox - tox +Install the ``pre-commit`` hook with: - # Or optionally - # (to skip tests for Python versions you do not have installed) - tox --skip-missing-interpreters +.. code-block:: bash -This will run through all of the tests and produce an output similar to:: + pre-commit install - ______________________________________________________ summary ______________________________________________________ - SKIPPED: py36: InterpreterNotFound: python3.6 - py37: commands succeeded - py38: commands succeeded - py39: commands succeeded - SKIPPED: py36-lxml: InterpreterNotFound: python3.6 - py37-lxml: commands succeeded - py38-lxml: commands succeeded - py39-lxml: commands succeeded - pep8: commands succeeded - docs: commands succeeded - congratulations :) +and check the code with: -You are primarily looking for the ``congratulations :)`` line at the bottom, -signifying that the code is working as expected on all configurations -available. +.. code-block:: bash -.. _tox: https://pypi.python.org/pypi/tox + pre-commit run --all-files -coverage -~~~~~~~~ +Running the Tests +----------------- -You can also run the tests with coverage_ to see which lines are covered by the -tests. This is useful for writing new tests to cover any uncovered lines:: +To run the tests, simply use: - pytest tests --cov=fastkml --cov=tests --cov-report=xml +.. code-block:: bash + pytest -pre-commit -~~~~~~~~~~~ +You can also run the tests with `coverage `_ +to see which lines are covered by the tests. +This is useful for writing new tests to cover any uncovered lines: -Install the ``pre-commit`` hook with:: +.. code-block:: bash - pip install pre-commit - pre-commit install + pytest --cov=fastkml --cov-report=term -and check the code with:: +To get a report on the individual lines that are not covered, use the +``--cov-report=term-missing`` option, or generate an HTML report with +``--cov-report=html``. +Some editor extensions can also show the coverage directly in the editor, notably +`coverage-gutter `_ +for VSCode, which needs the output to be in the ``xml`` format produced with +``--cov-report=xml``. - pre-commit run --all-files + +Tips +---- + +- Commit often, commit early. +- Make a draft PR while you are still working on it to give your work some visibility. diff --git a/fastkml/about.py b/fastkml/about.py index 9a828a65..ee30bf21 100644 --- a/fastkml/about.py +++ b/fastkml/about.py @@ -18,4 +18,4 @@ The only purpose of this module is to provide a version number for the package. """ -__version__ = "1.0.a12" +__version__ = "1.0.a13" diff --git a/fastkml/atom.py b/fastkml/atom.py index e29fa3c4..1a75b8dd 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 @@ -62,7 +64,7 @@ class _AtomObject(_XMLObject): The atom tag name is the class name in lower case. """ - _default_ns = config.ATOMNS + _default_nsid = config.ATOM @classmethod def get_tag_name(cls) -> str: @@ -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,31 +84,11 @@ 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__( self, @@ -118,6 +102,37 @@ def __init__( 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}, " @@ -138,33 +161,27 @@ def __repr__(self) -> str: f"hreflang={self.hreflang!r}, " f"title={self.title!r}, " f"length={self.length!r}, " - f"**kwargs={self._get_splat()!r}," + f"**{self._get_splat()!r}," ")" ) def __bool__(self) -> bool: - return bool(self.href) + """ + Check if the Link object is truthy. - def __eq__(self, other: object) -> bool: - try: - assert isinstance(other, type(self)) - except AssertionError: - return False - return ( - super().__eq__(other) - and self.href == other.href - and self.rel == other.rel - and self.type == other.type - and self.hreflang == other.hreflang - and self.title == other.title - and self.length == other.length - ) + Returns + ------- + bool + True if the Link object has a href attribute, False otherwise. + + """ + return bool(self.href) registry.register( Link, item=RegistryItem( - ns_ids=("atom", ""), + ns_ids=("", "atom"), attr_name="href", node_name="href", classes=(str,), @@ -175,7 +192,7 @@ def __eq__(self, other: object) -> bool: registry.register( Link, item=RegistryItem( - ns_ids=("atom", ""), + ns_ids=("", "atom"), attr_name="rel", node_name="rel", classes=(str,), @@ -186,7 +203,7 @@ def __eq__(self, other: object) -> bool: registry.register( Link, item=RegistryItem( - ns_ids=("atom", ""), + ns_ids=("", "atom"), attr_name="type", node_name="type", classes=(str,), @@ -197,7 +214,7 @@ def __eq__(self, other: object) -> bool: registry.register( Link, item=RegistryItem( - ns_ids=("atom", ""), + ns_ids=("", "atom"), attr_name="hreflang", node_name="hreflang", classes=(str,), @@ -209,7 +226,7 @@ def __eq__(self, other: object) -> bool: registry.register( Link, item=RegistryItem( - ns_ids=("atom", ""), + ns_ids=("", "atom"), attr_name="title", node_name="title", classes=(str,), @@ -220,7 +237,7 @@ def __eq__(self, other: object) -> bool: registry.register( Link, item=RegistryItem( - ns_ids=("atom", ""), + ns_ids=("", "atom"), attr_name="length", node_name="length", classes=(int,), @@ -232,19 +249,19 @@ 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__( self, @@ -255,13 +272,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}, " @@ -269,24 +306,20 @@ def __repr__(self) -> str: f"name={self.name!r}, " f"uri={self.uri!r}, " f"email={self.email!r}, " - f"**kwargs={self._get_splat()!r}," + f"**{self._get_splat()!r}," ")" ) def __bool__(self) -> bool: - return bool(self.name) + """ + Check if the _Person object has a name. - def __eq__(self, other: object) -> bool: - try: - assert isinstance(other, type(self)) - except AssertionError: - return False - return ( - super().__eq__(other) - and self.name == other.name - and self.uri == other.uri - and self.email == other.email - ) + Returns + ------- + bool: True if the _Person object has a name, False otherwise. + + """ + return bool(self.name) registry.register( diff --git a/fastkml/base.py b/fastkml/base.py index c8a8d6f6..15c00da7 100644 --- a/fastkml/base.py +++ b/fastkml/base.py @@ -31,11 +31,13 @@ logger = logging.getLogger(__name__) +__all__ = ["_XMLObject"] + class _XMLObject: """XML Baseclass.""" - _default_ns: str = "" + _default_nsid: str = "" _node_name: str = "" name_spaces: Dict[str, str] __kwarg_keys: Tuple[str, ...] @@ -46,40 +48,100 @@ def __init__( name_spaces: Optional[Dict[str, str]] = None, **kwargs: Any, ) -> None: - """Initialize the XML Object.""" - self.ns: str = self._default_ns if ns is None else ns + """ + 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 in kwargs: setattr(self, arg, kwargs[arg]) self.__kwarg_keys = tuple(kwargs.keys()) def __repr__(self) -> str: - """Create a string (c)representation for _XMLObject.""" + """ + 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"**kwargs={self._get_splat()!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. + + """ if type(self) is not type(other): return False - assert isinstance(other, type(self)) - return self.ns == other.ns and self.name_spaces == other.name_spaces + assert isinstance(other, type(self)) # noqa: S101 + return self.__dict__ == other.__dict__ def etree_element( self, precision: Optional[int] = None, verbosity: Verbosity = Verbosity.normal, ) -> Element: - """Return the KML Object as an Element.""" - element: Element = config.etree.Element( # type: ignore[attr-defined] + """ + Return the KML Object as an Element. + + 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__): @@ -100,7 +162,24 @@ def to_string( precision: Optional[int] = None, verbosity: Verbosity = Verbosity.normal, ) -> str: - """Return the KML Object as serialized xml.""" + """ + 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, @@ -108,7 +187,7 @@ def to_string( try: return cast( str, - config.etree.tostring( # type: ignore[attr-defined] + config.etree.tostring( element, encoding="UTF-8", pretty_print=prettyprint, @@ -119,7 +198,7 @@ def to_string( except TypeError: return cast( str, - config.etree.tostring( # type: ignore[attr-defined] + config.etree.tostring( element, encoding="UTF-8", ).decode( @@ -128,6 +207,15 @@ def to_string( ) 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 @@ -136,12 +224,36 @@ def _get_splat(self) -> Dict[str, Any]: @classmethod def get_tag_name(cls) -> str: - """Return the tag name.""" + """ + Return the tag name. + + Returns + ------- + str + The tag name. + + """ return cls.__name__ @classmethod - def _get_ns(cls, ns: Optional[str]) -> str: - return cls._default_ns if ns is None else ns + 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 @classmethod def _get_kwargs( @@ -152,13 +264,31 @@ def _get_kwargs( element: Element, strict: bool, ) -> Dict[str, Any]: - """Returns a dictionary of kwargs for the class constructor.""" + """ + Get the keyword arguments for the class constructor. + + 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: - # breakpoint() kwarg = item.get_kwarg( element=element, ns=name_spaces.get(name_space, ""), @@ -184,7 +314,26 @@ def class_from_element( element: Element, strict: bool, ) -> Self: - """Creates an XML object from an etree element.""" + """ + 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, @@ -205,24 +354,34 @@ def class_from_string( strict: bool = True, ) -> Self: """ - Creates a geometry object from a string. + Create an XML object from a string. - Args: - ---- - string: String representation of the geometry object + 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: + Returns ------- - Geometry object + Self + The XML object. """ - ns = cls._get_ns(ns) + 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), # type: ignore[attr-defined] + config.etree.fromstring(string), ), ) diff --git a/fastkml/config.py b/fastkml/config.py index a8ec88dc..4756d01c 100644 --- a/fastkml/config.py +++ b/fastkml/config.py @@ -25,6 +25,7 @@ "DEFAULT_NAME_SPACES", "GXNS", "KMLNS", + "etree", "register_namespaces", "set_default_namespaces", "set_etree_implementation", diff --git a/fastkml/containers.py b/fastkml/containers.py index e513701b..bf1fa398 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 that holds one or more Features. + + Supports the creation of nested hierarchies. subclasses are: Document, Folder. @@ -146,7 +146,7 @@ def __repr__(self) -> str: f"region={self.region!r}, " f"extended_data={self.extended_data!r}, " f"features={self.features!r}, " - f"**kwargs={self._get_splat()!r}," + f"**{self._get_splat()!r}," ")" ) @@ -155,21 +155,23 @@ 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. """ @@ -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, @@ -250,11 +286,25 @@ def __repr__(self) -> str: f"extended_data={self.extended_data!r}, " f"features={self.features!r}, " f"schemata={self.schemata!r}, " - f"**kwargs={self._get_splat()!r}," + f"**{self._get_splat()!r}," ")" ) 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) diff --git a/fastkml/data.py b/fastkml/data.py index 6dc1bb24..23dafdfb 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}, " @@ -113,11 +140,19 @@ def __repr__(self) -> str: f"name={self.name!r}, " f"type={self.type}, " f"display_name={self.display_name!r}, " - f"**kwargs={self._get_splat()!r}," + f"**{self._get_splat()!r}," ")" ) 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}, " @@ -203,12 +272,20 @@ def __repr__(self) -> str: f"target_id={self.target_id!r}, " f"name={self.name!r}, " f"fields={self.fields!r}, " - f"**kwargs={self._get_splat()!r}," + f"**{self._get_splat()!r}," ")" ) 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}, " @@ -276,11 +381,20 @@ def __repr__(self) -> str: f"name={self.name!r}, " f"value={self.value!r}, " f"display_name={self.display_name!r}, " - f"**kwargs={self._get_splat()!r}," + f"**{self._get_splat()!r}," ")" ) 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}, " @@ -354,11 +490,19 @@ def __repr__(self) -> str: f"target_id={self.target_id!r}, " f"name={self.name!r}, " f"value={self.value!r}, " - f"**kwargs={self._get_splat()!r}," + f"**{self._get_splat()!r}," ")" ) 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}, " @@ -432,14 +593,31 @@ def __repr__(self) -> str: f"target_id={self.target_id!r}, " f"schema_url={self.schema_url!r}, " f"data={self.data!r}, " - f"**kwargs={self._get_splat()!r}," + f"**{self._get_splat()!r}," ")" ) 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}, " @@ -505,11 +703,20 @@ def __repr__(self) -> str: f"id={self.id!r}, " f"target_id={self.target_id!r}, " f"elements={self.elements!r}, " - f"**kwargs={self._get_splat()!r}," + f"**{self._get_splat()!r}," ")" ) 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..4f809f0f 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,23 +108,60 @@ 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}, " f"name_spaces={self.name_spaces!r}, " f"text={self.text!r}, " f"max_lines={self.max_lines!r}, " - f"**kwargs={self._get_splat()!r}," + f"**{self._get_splat()!r}," ")" ) 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,30 @@ def __bool__(self) -> bool: class _Feature(TimeMixin, _BaseObject): """ - abstract element; do not create - subclasses are: - * Container (Document, Folder) - * Placemark - * Overlay - * NetworkLink. + Abstract base class representing a feature in KML. + + Direct known subclasses: + - Container (Document, Folder) + - Placemark + - Overlay + - NetworkLink + - 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