diff --git a/fastkml/kml.py b/fastkml/kml.py index e5b4bbda..ddc7097a 100644 --- a/fastkml/kml.py +++ b/fastkml/kml.py @@ -26,7 +26,10 @@ """ import logging +from pathlib import Path +from typing import IO from typing import Any +from typing import AnyStr from typing import Dict from typing import Iterable from typing import List @@ -199,6 +202,56 @@ def class_from_string( element=element, ) + @classmethod + def parse( + cls, + file: Union[Path, str, IO[AnyStr]], + *, + ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, + strict: bool = True, + ) -> Self: + """ + Parse a KML file and return a KML object. + + Args: + ---- + file: The file to parse + + Keyword Args: + ------------ + ns (Optional[str]): The namespace of the KML file. + 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. + + Returns: + ------- + KML object: The parsed KML object. + + """ + try: + tree = config.etree.parse( + file, + parser=config.etree.XMLParser( + huge_tree=True, + recover=True, + ), + ) + except TypeError: + tree = config.etree.parse(file) + root = tree.getroot() + if ns is None: + ns = cast(str, root.tag[:-3] if root.tag.endswith("kml") else "") + name_spaces = name_spaces or {} + name_spaces = {**config.NAME_SPACES, **name_spaces} + return cls.class_from_element( + ns=ns, + name_spaces=name_spaces, + strict=strict, + element=root, + ) + registry.register( KML, diff --git a/tests/kml_test.py b/tests/kml_test.py index 1cc9ef51..313d3b73 100644 --- a/tests/kml_test.py +++ b/tests/kml_test.py @@ -15,14 +15,20 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA """Test the kml class.""" +import pathlib import pygeoif as geo from fastkml import features from fastkml import kml +from fastkml.containers import Document +from fastkml.features import Placemark from tests.base import Lxml from tests.base import StdLibrary +BASEDIR = pathlib.Path(__file__).parent +KMLFILEDIR = BASEDIR / "ogc_conformance" / "data" / "kml" + class TestStdLibrary(StdLibrary): """Test with the standard library.""" @@ -52,6 +58,77 @@ def test_kml(self) -> None: assert k.to_string() == k2.to_string() +class TestParseKML(StdLibrary): + def test_parse_kml(self) -> None: + empty_placemark = KMLFILEDIR / "emptyPlacemarkWithoutId.xml" + + doc = kml.KML.parse(empty_placemark) + + assert doc == kml.KML( + ns="{http://www.opengis.net/kml/2.2}", + features=[ + Document( + ns="{http://www.opengis.net/kml/2.2}", + id="doc-001", + target_id="", + name="Vestibulum eleifend lobortis lorem.", + features=[ + Placemark( + ns="{http://www.opengis.net/kml/2.2}", + ), + ], + schemata=[], + ), + ], + ) + + def test_parse_kml_filename(self) -> None: + empty_placemark = str(KMLFILEDIR / "emptyPlacemarkWithoutId.xml") + + doc = kml.KML.parse(empty_placemark) + + assert doc == kml.KML( + ns="{http://www.opengis.net/kml/2.2}", + features=[ + Document( + ns="{http://www.opengis.net/kml/2.2}", + id="doc-001", + target_id="", + name="Vestibulum eleifend lobortis lorem.", + features=[ + Placemark( + ns="{http://www.opengis.net/kml/2.2}", + ), + ], + schemata=[], + ), + ], + ) + + def test_parse_kml_fileobject(self) -> None: + empty_placemark = KMLFILEDIR / "emptyPlacemarkWithoutId.xml" + with empty_placemark.open() as f: + doc = kml.KML.parse(f) + + assert doc == kml.KML( + ns="{http://www.opengis.net/kml/2.2}", + features=[ + Document( + ns="{http://www.opengis.net/kml/2.2}", + id="doc-001", + target_id="", + name="Vestibulum eleifend lobortis lorem.", + features=[ + Placemark( + ns="{http://www.opengis.net/kml/2.2}", + ), + ], + schemata=[], + ), + ], + ) + + class TestLxml(Lxml, TestStdLibrary): """Test with lxml.""" @@ -65,3 +142,7 @@ def test_from_string_with_unbound_prefix(self) -> None: k = kml.KML.class_from_string(doc) assert len(k.features) == 1 assert isinstance(k.features[0], features.Placemark) + + +class TestLxmlParseKML(Lxml, TestParseKML): + """Test with Lxml."""